├── .gitignore
├── LICENSE
├── README.md
├── demo
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.tsx
│ ├── components
│ │ ├── Api.tsx
│ │ ├── Copywriting.tsx
│ │ ├── CopywritingInput.tsx
│ │ ├── CopywritingOutput.tsx
│ │ ├── Storywriting.tsx
│ │ ├── StorywritingInput.tsx
│ │ └── StorywritingOutput.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ └── setupTests.ts
└── tsconfig.json
├── jest.config.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── components
│ ├── Cell
│ │ ├── Cell.stories.tsx
│ │ ├── Cell.test.tsx
│ │ ├── Cell.tsx
│ │ ├── Cell.types.ts
│ │ ├── CellBoard.stories.tsx
│ │ ├── CellBoard.tsx
│ │ ├── CellEditor.stories.tsx
│ │ ├── CellEditor.tsx
│ │ ├── CellTree.stories.tsx
│ │ └── CellTree.tsx
│ ├── Generator
│ │ ├── Generator.stories.tsx
│ │ ├── Generator.test.tsx
│ │ ├── Generator.tsx
│ │ └── Generator.types.ts
│ └── Lens
│ │ ├── Lens.stories.tsx
│ │ ├── Lens.test.tsx
│ │ ├── Lens.tsx
│ │ ├── Lens.types.ts
│ │ ├── ListLens.tsx
│ │ ├── PlotLens.tsx
│ │ ├── RatingLens.tsx
│ │ └── SpaceLens.tsx
├── context
│ ├── ObjectsContextProvider.stories.tsx
│ ├── ObjectsContextProvider.test.tsx
│ ├── ObjectsContextProvider.tsx
│ └── Storywriting.stories.tsx
├── index.ts
└── utils
│ └── getLoremIpsum.tsx
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 2-Clause License
2 |
3 | Copyright (c) 2023, KIXLAB
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LLM-UI-Objects Library
2 |
3 | ## Overview
4 |
5 | The LLM-UI-Objects library provides a set of object components that enable the creation of Large Language Model (LLM)-powered writing interfaces that support object-oriented interaction. Object-oriented interaction can enable users to more flexibly experiment and iterate with LLMs. The objects supported in the library are: **Cells** (input text fragments), **Generators** (sets of model parameters), and **Lenses** (output containers). This library is an instantiation of the [Cells, Generators, and Lenses](https://llm-objects.kixlab.org/) design framework that was presenteed at UIST 2023.
6 |
7 | ## Getting Started
8 |
9 | Install by executing `npm install llm-ui-objects`.
10 | Refer to directory `demo` for examples of how to import and use the components.
11 |
12 | ## Components
13 |
14 | Below is an overview of the different components available in the library.
15 |
16 | | Name | Description | Properties |
17 | | --------- | ----------- | ----------- |
18 | | `ObjectContextProvider` | Contains necessary context and information on the existing objects, their contents, and properties. Supports management of the objects by providing various helper functions that allow controlling, manipulating, and transferring information between these objects. |
`children`: Component(s) that the context provider wraps around. `cells`: Array of initial set of cells. `generators`: Array of initial set of generators. `lenses`: Array of initial set of lenses. `generateHandler`: Handler function that can generate text or list of text based on input text or list of text and model parameters. `minimizeHandler`: Handler function that can process text to produce a minimized version of the text. |
19 | | `Cell` | Represents a text fragment as an object. Cells can be connected to each other to represent a complete text input. | `id`: Unique string ID. `text`: Text fragment contained within cell object. `isActive`: Controls whether the cell is activated and will be used when generating. `isSelected`: Controls whether the cell is selected. Selected cells can be copied, deleted, etc. `isHovered`: Controls whether the cell is being hovered on. `isMinimized`: Controls whether the cell is minimized. If it is, it will only show the minimized text. `minimizedText`: Version of the text that is shown when cell is minimized. `tabDirection`: Can take values "top", "right", "bottom, or "left" and determines what side of the cell will have additional padding space. `onClick`, `onMouseEnter`, `onMouseLeave`: Handlers for mouse events. `parentCellId`: ID of cell that this cell is connected to. `style`: Style for the cell container. |
20 | | `Generator` | Represents a set of model parameter configurations and can be clicked on to generate outputs. Generators can be connected to cells to take its text (and text of all cells connected from that cell) as input. Generators can also be connected to lenses where its generation outputs will be contained and represented. | `id`: Unique string ID. `parameters`: Array of parameter objects, where each object contains the parameters information and the current parameter value. `color`: Color of the generator. `size`: Can take values "small", "medium" and "large". `numColumns`: Number of parameters that are shown in each row of the generator. `isGenerating`: Controls whether the generator is generating. `isSelected`: Controls whether the generator is selected. `cellId`: ID of cell connected to the generator. `lensId`: Id of lens connected to the generator. `onMouseEnter`, `onMouseLeave`: Mouse event handlers. |
21 | | `Lens` | Represents the containers of generated outputs and can represent the outputs in various ways to support exploration of these outputs. Lenses can be connected together by assigning to the same group: lenses in the same group will show the same generated outputs. Examples of these representations are shown in the table below (e.g., `ListLens`, `SpaceLens`) | `id`: Unique string ID. `type`: Determines the type of lens. Currently takes the values "list", "space", "rating" or "plot". `style`: Style of the lens container. `onGenerationClick`: Handler function for when a generation is clicked in the lens. `group`: Group number that the lens belongs to. `getGenerationMetadata`: Handler function that processes outputs to produce metadata that is used to represent these outputs (e.g., `getPosition` or `getRating` for the `SpaceLens` and `RatingLens` respectively). |
22 |
23 | For the **Cell** and **Lens** objects, we also provide a couple of examples of the different forms that these objects can take.
24 |
25 | | Name | Description | Properties |
26 | | --------- | ----------- | ----------- |
27 | | `CellBoard` | A board of cells where the user can create multiple rows of cells and multiple cells within these rows. The user selects individual cells in each row to compose an input. | `initialBoard`: 2d-array that contains strings that are used to create cells and populate the board. `maxRows` and `maxColumns`: Maximum number of rows and columns that the board can contain. `setEntryCell`: Helper function to obtain the leaf-most cell, which can be used to concatenate the full input of selected cells. `style`: Style for the board container. |
28 | | `CellTree` | A tree representation of cells where parent-child relationships represent sentences that are continuations to each other. Each cells is minimized: they are represented by a rectangle containing the minimized text for the cell. | `cellWidth` and `cellheight`: Width and height of each individual cell block in the tree. `style`: Style for the tree container. |
29 | | `CellEditor` | A QuillJS-based text editor that shows the contatenation of multiple selected cells and allows editing of individual cells directly on the editor. | `cellIds`: IDs of cells to be shown in the text editor. `style`: Style for the editor container. `textColor`: Color of text in the editor. |
30 | | `ListLens` | A lens that shows different outputs as a hierarchical list, where outputs are first grouped based on the input that was used to generate them and then grouped based on the model parameter settings used to generate them. | `generations`: Array of generated outputs. `onGenerationClick`: Handler function for when a generation is clicked in the lens. |
31 | | `SpaceLens` | A lens that shows different outputs as dots in a 2D space where their position of each output is specified by the `getPosition` function passed to the component. | `generations`: ... `onGenerationClick`: ... `getPosition`: Calculates the positon of outputs (x, y coordinates) to represent in the lens. |
32 | | `PlotLens` | A lens that shows different outputs as dots in a scatter plot where the user can select the axis for the plot, and the value of the dots for each axis are determined by the `getRatings` function passed to the component. | `generations`: ... `onGenerationClick`: ... `getRatings`: Calculates the ratings of outputs on multiple dimensions. |
33 | | `RatingLens` | A lens that shows the ratings given to each output on multiple dimensions where the ratings of these outputs are determined by the `getRatings` function passed to the component | `generations`: ... `onGenerationClick`: ... `getRatings`: ... |
34 |
35 |
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/demo/README.md:
--------------------------------------------------------------------------------
1 | # LLM UI Objects Demo
2 |
3 | This demo shows how the LLM UI Objects library can be used to create a copywriting and storywriting interface.
4 |
5 | ## Getting Started
6 |
7 | Install required libraries by using `npm install`.
8 |
9 | Then, run the app in development mode by ussing `npm run start`.\
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | In the interface, click on the key icon at the top right and input your OpenAI API key.\
13 | Then, you can switch between the Copywriting and Storywriting interface to test the LLM UI objects.
14 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.17.0",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "@types/jest": "^27.5.2",
10 | "@types/node": "^16.18.59",
11 | "@types/react": "^18.2.31",
12 | "@types/react-dom": "^18.2.14",
13 | "llm-ui-objects": "^1.3.0",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0",
16 | "react-scripts": "5.0.1",
17 | "typescript": "^4.9.5",
18 | "web-vitals": "^2.1.4"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/demo/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
16 |
17 |
26 | LLM UI Objects
27 |
28 |
29 | You need to enable JavaScript to run this app.
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/demo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/demo/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import { key } from './components/Api';
5 |
6 | import Storywriting from './components/Storywriting';
7 | import Copywriting from './components/Copywriting';
8 |
9 | const KeySvg =
10 |
11 |
12 |
13 | ;
14 |
15 | const Container = styled.div`
16 | width: calc(100% - 64px);
17 | height: calc(100% - 48px);
18 | background-color: #eee;
19 | position: relative;
20 | z-index: 0;
21 | padding: 16px 32px 32px 32px;
22 | display: flex;
23 | flex-direction: column;
24 | `;
25 |
26 | const Header = styled.div`
27 | display: flex;
28 | flex-direction: row;
29 | justify-content: space-between;
30 | padding-bottom: 16px;
31 | `;
32 |
33 | const KeyContainer = styled.div`
34 | display: flex;
35 | flex-direction: row;
36 | gap: 4px;
37 | justify-content: flex-end;
38 | flex: 1;
39 | `;
40 |
41 | const KeyInput = styled.input`
42 | width: 200px;
43 | height: 32px;
44 | padding: 4px 8px;
45 | border: none;
46 | border-radius: 4px;
47 | font-size: 12px;
48 | box-sizing: border-box;
49 | `;
50 |
51 | const KeyButton = styled.div`
52 | width: 32px;
53 | height: 32px;
54 | margin-left: 4px;
55 | border-radius: 4px;
56 | cursor: pointer;
57 | background-color: #ccc;
58 | fill: #fff;
59 | box-sizing: border-box;
60 | display: flex;
61 | justify-content: center;
62 | align-items: center;
63 | `;
64 |
65 | const AppSwitcher = styled.div`
66 | display: flex;
67 | flex-direction: row;
68 | gap: 8px;
69 | flex: 1;
70 | `;
71 |
72 | const AppButton = styled.div<{selected?: boolean}>`
73 | width: 140px;
74 | height: 32px;
75 | border-radius: 4px;
76 | cursor: pointer;
77 | background-color: ${(props) => props.selected ? '#0088ff' : '#ccc'};
78 | color: #fff;
79 | box-sizing: border-box;
80 | display: flex;
81 | justify-content: center;
82 | align-items: center;
83 | font-weight: bold;
84 | `;
85 |
86 | function App() {
87 | const [keyOpen, setKeyOpen] = React.useState(false);
88 | const [keyInput, setKeyInput] = React.useState(key.value);
89 |
90 | const [app, setApp] = React.useState('storywriting');
91 |
92 | return (
93 | setKeyOpen(false)}>
94 |
95 |
96 | setApp("storywriting")}
99 | >
100 | Storywriting
101 |
102 | setApp("copywriting")}
105 | >
106 | Copywriting
107 |
108 |
109 |
110 | {
113 | key.value = e.target.value;
114 | setKeyInput(e.target.value);
115 | }}
116 | onClick={(e: any) => e.stopPropagation()}
117 | style={{display: keyOpen ? 'block' : 'none'}}
118 | value={keyInput}
119 | type="password"
120 | />
121 | {
123 | e.stopPropagation();
124 | setKeyOpen(!keyOpen);
125 | }}
126 | >
127 | {KeySvg}
128 |
129 |
130 |
131 |
132 |
133 |
134 | );
135 | }
136 |
137 | export default App;
138 |
--------------------------------------------------------------------------------
/demo/src/components/Api.tsx:
--------------------------------------------------------------------------------
1 | import { GenerationProps } from "llm-ui-objects";
2 |
3 | var key: { value: string } = { value: "" };
4 |
5 | const generate = (system: string, input: string | string[], parameters: any) => {
6 | var url = "https://api.openai.com/v1/chat/completions";
7 | var bearer = 'Bearer ' + key.value;
8 |
9 | if(typeof input === 'string') input = [input];
10 |
11 | const promises: Promise[] = [];
12 |
13 | input.forEach((item) => {
14 | promises.push(new Promise((resolve, reject) => {
15 | fetch(url, {
16 | method: 'POST',
17 | headers: {
18 | 'Authorization': bearer,
19 | 'Content-Type': 'application/json'
20 | },
21 | body: JSON.stringify({
22 | ...parameters,
23 | "messages": [
24 | {
25 | "role": "system",
26 | "content": system
27 | },
28 | {
29 | "role": "user",
30 | "content": item
31 | }
32 | ]
33 | })
34 | })
35 | .then(response => response.json())
36 | .then(data => {
37 | resolve(data.choices[0].message.content);
38 | })
39 | .catch((error) => {
40 | reject(error);
41 | });
42 | }));
43 | });
44 |
45 | return Promise.all(promises);
46 | };
47 |
48 | const getPositions = async (generations: GenerationProps[]) => {
49 | var url = "https://api.openai.com/v1/embeddings";
50 | var bearer = 'Bearer ' + key.value;
51 |
52 | if(generations.length === 0) return Promise.resolve([]);
53 |
54 | const result = await fetch(url, {
55 | method: 'POST',
56 | headers: {
57 | 'Authorization': bearer,
58 | 'Content-Type': 'application/json'
59 | },
60 | body: JSON.stringify({
61 | "input": generations.map((generation) => generation.content),
62 | "model": "text-embedding-ada-002",
63 | "encoding_format": "float"
64 | })
65 | })
66 | .then(response => response.json())
67 | .then(response => {
68 | var embeddingsList = response.data.map((item: any) => item.embedding);
69 | var positions = embeddingsList.map((item: any) => {
70 | var averageFirstHalf = item.slice(0, item.length / 2).reduce((a: any, b: any) => a + b) / (item.length / 2);
71 | var averageSecondHalf = item.slice(item.length / 2, item.length).reduce((a: any, b: any) => a + b) / (item.length / 2);
72 | return {x: averageFirstHalf, y: averageSecondHalf};
73 | });
74 | return positions;
75 | })
76 | .catch((error) => {
77 | console.error(error);
78 | return [];
79 | });
80 |
81 | return result;
82 | }
83 |
84 | const getRatings = (generations: GenerationProps[]) => {
85 | const prompt = `You are a helpful and precise assistant that can check the quality of writing by another AI assistant. You should evaluate how well a piece of writing satisfies a set of quality criteria. For each criterion, provide a one sentence explanation on how the writing satisfies the criterion and then provide a score out of 20 for that criterion. You should return your final answer as a valid JSON object.
86 |
87 | [The Start of Criteira]
88 | Creative: The writing is creative and original. It should include novel ideas and concepts.
89 | Simple: The writing is simple and easy to understand. It should be able to be understood by a wide audience.
90 | Positive: The writing is positive and uplifting. It should be able to make the reader feel good.
91 | Concise: The writing is concise and to the point. It should be able to convey its message in a short amount of time.
92 | Implicit: The writing is implicit and leaves room for interpretation. It should be able to be interpreted in multiple ways.
93 | [The End of Criteria]
94 |
95 | When returning your response, first output the token "$$$ANSWER$$$" and then output your answer as a valid JSON object of the following format:
96 | {"Creative": {"explanation": , "score": }, "Simple": {"explanation": , "score": }, "Positive": {"explanation": , "score": }, "Concise": {"explanation": , "score": }, "Implicit": {"explanation": , "score": }}`;
97 |
98 | return generate(prompt, generations.map((item) => item.content), {
99 | "model": "gpt-3.5-turbo",
100 | "temperature": 0.3,
101 | "max_tokens": 256
102 | }).then((response) => {
103 | var answers = response.map((item) => {
104 | var answer = JSON.parse(item.split("$$$ANSWER$$$")[1]);
105 | var ratings: {[key: string]: number} = {};
106 | Object.keys(answer).forEach((key) => {
107 | ratings[key] = answer[key].score / 20;
108 | });
109 | return ratings;
110 | });
111 | return answers;
112 | });
113 | }
114 |
115 | export { key, generate, getPositions, getRatings };
116 |
--------------------------------------------------------------------------------
/demo/src/components/Copywriting.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { ObjectsContextProvider, CellProps, GeneratorProps, ParameterProps, LensProps } from "llm-ui-objects";
4 |
5 | import { generate } from "./Api"
6 | import CopywritingInput from "./CopywritingInput";
7 | import CopywritingOutput from "./CopywritingOutput";
8 |
9 | const defaultParameters: ParameterProps[] = [
10 | {
11 | id: "model",
12 | name: "Model",
13 | value: "gpt-3.5-turbo",
14 | type: "nominal",
15 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "gpt-3.5-turbo-0301", "gpt-4-0314"],
16 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "gpt-3.5-turbo-0301": "3.5M", "gpt-4-0314": "4M"},
17 | defaultValue: "gpt-3.5-turbo"
18 | },
19 | {
20 | id: "temperature",
21 | name: "Temperature",
22 | nickname: "temp",
23 | value: 0.7,
24 | type: "continuous",
25 | allowedValues: [0.0, 2.0],
26 | defaultValue: 1.0
27 | },
28 | {
29 | id: "presence_penalty",
30 | name: "Presence Penalty",
31 | nickname: "present",
32 | value: 0.0,
33 | type: "continuous",
34 | allowedValues: [0.0, 2.0],
35 | defaultValue: 0.0
36 | },
37 | {
38 | id: "frequency_penalty",
39 | name: "Frequency Penalty",
40 | nickname: "frequent",
41 | value: 0.0,
42 | type: "continuous",
43 | allowedValues: [0.0, 2.0],
44 | defaultValue: 0.0
45 | }
46 | ];
47 |
48 | const defaultGenerators: GeneratorProps[] = [
49 | {
50 | id: "generator-0",
51 | parameters: JSON.parse(JSON.stringify(defaultParameters)),
52 | color: "#0088ff",
53 | size: "medium",
54 | isGenerating: false,
55 | isSelected: false,
56 | cellId: null,
57 | lensId: "lens-0"
58 | }
59 | ];
60 |
61 | const defaultLens: LensProps[] = [
62 | {
63 | id: "lens-0",
64 | type: "list",
65 | group: 0
66 | },
67 | {
68 | id: "lens-1",
69 | type: "rating",
70 | group: 0
71 | }
72 | ];
73 |
74 | const generateAd = (input: string | string[], parameters: any) => {
75 | const systemPrompt = "You are a creative writing assistant. You will be given a couple of requirements for a creative advertisement, and you should create and advertisement that is at most 6 sentences long.";
76 | return generate(systemPrompt, input, parameters);
77 | }
78 |
79 | const Container = styled.div`
80 | height: 100%;
81 | display: flex;
82 | flex-direction: row;
83 | gap: 32px;
84 | `;
85 |
86 | const Copywriting = ({display}: {display: boolean}) => {
87 | return (
88 | Promise}
92 | >
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
100 |
101 | export default Copywriting;
--------------------------------------------------------------------------------
/demo/src/components/CopywritingInput.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { ObjectsContext, CellBoard, Generator } from "llm-ui-objects";
5 |
6 | const PlusSvg = ;
7 |
8 | const Container = styled.div`
9 | display: flex;
10 | flex-direction: column;
11 | gap: 8px;
12 | height: 100%;
13 | width: 40%;
14 | `;
15 |
16 | const GeneratorTray = styled.div`
17 | width: 100%;
18 | display: flex;
19 | flex-direction: column;
20 | gap: 8px;
21 |
22 | & > div:first-child {
23 | width: 100%;
24 | display: flex;
25 | flex-direction: row;
26 | gap: 8px;
27 | justify-content: center;
28 | align-items: center;
29 | }
30 |
31 | & > div:last-child {
32 | width: 100%;
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | }
37 | `;
38 |
39 | const AddButton = styled.div`
40 | align-items: center;
41 | background-color: #0066ff;
42 | fill: #fff;
43 | border-radius: 4px;
44 | cursor: pointer;
45 | display: flex;
46 | justify-content: center;
47 | height: 40px;
48 | width: 40px;
49 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.2);
50 | `;
51 |
52 | const CopywritingInput = ({parameters}: {parameters: any}) => {
53 | const [entryCell, setEntryCell] = React.useState(null);
54 | const { generators, addGenerator, linkCellToGenerator } = React.useContext(ObjectsContext);
55 |
56 | React.useEffect(() => {
57 | if(!entryCell) return;
58 | generators.forEach((generator) => {
59 | if(generator.cellId === entryCell) return;
60 | linkCellToGenerator(entryCell, generator.id);
61 | });
62 | }, [entryCell]);
63 |
64 | const handleAddGenerator = () => {
65 | addGenerator({
66 | id: "placeholder",
67 | color: "#0088ff",
68 | size: "medium",
69 | parameters: JSON.parse(JSON.stringify(parameters)),
70 | cellId: entryCell,
71 | lensId: "lens-0"
72 | });
73 | }
74 |
75 | return (
76 |
77 | setEntryCell(cellId || null)}
87 | style={{
88 | width: "100%",
89 | flex: "1",
90 | backgroundColor: "#fff",
91 | padding: "32px 24px",
92 | borderRadius: "8px",
93 | boxSizing: "border-box",
94 | boxShadow: "0px 4px 4px 0px rgba(0, 0, 0, 0.25)"
95 | }}
96 | />
97 |
98 |
99 | {generators.map((generator) => (
100 |
104 | ))}
105 |
106 |
107 |
108 | {PlusSvg}
109 |
110 |
111 |
112 |
113 | )
114 | }
115 |
116 | export default CopywritingInput;
--------------------------------------------------------------------------------
/demo/src/components/CopywritingOutput.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | import { ObjectsContext, Lens } from "llm-ui-objects";
5 |
6 | import { getPositions, getRatings } from "./Api";
7 |
8 | const ListSvg = ;
9 | const SpaceSvg = ;
10 | const PlotSvg = ;
11 | const TrashSvg = ;
12 |
13 | const Container = styled.div`
14 | display: flex;
15 | flex-direction: column;
16 | gap: 16px;
17 | height: 100%;
18 | width: 58%;
19 | `;
20 |
21 | const TextareaContainer = styled.div`
22 | flex: 1;
23 | width: 100%;
24 |
25 | & > textarea {
26 | width: 100%;
27 | height: 100%;
28 | border: none;
29 | border-radius: 8px;
30 | font-size: 18px;
31 | padding: 16px;
32 | resize: none;
33 | color: ${props => props.color};
34 | background-color: #fff;
35 | overflow-y: auto;
36 | box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.1);
37 | outline: none;
38 | font-family: "Roboto", sans-serif;
39 | box-sizing: border-box;
40 | transition: color 0.5s ease;
41 |
42 | &::-webkit-scrollbar {
43 | width: 4px;
44 | }
45 | &::-webkit-scrollbar-track {
46 | background: #f1f1f1;
47 | border-radius: 4px;
48 | }
49 | &::-webkit-scrollbar-thumb {
50 | background: #ddd;
51 | border-radius: 4px;
52 | }
53 | }
54 | `;
55 |
56 | const LensesContainer = styled.div`
57 | width: 100%;
58 | display: flex;
59 | flex-direction: row;
60 | gap: 8px;
61 | flex: 1;
62 | `;
63 |
64 | const LensWrapper = styled.div`
65 | flex: 1;
66 | border: solid 2px #0088ff;
67 | border-radius: 8px;
68 | box-shadow: 0px 4px 4px 2px rgba(0, 0, 0, 0.2);
69 | padding: 8px;
70 | background-color: #fff;
71 | box-sizing: border-box;
72 | display: flex;
73 | flex-direction: column;
74 | gap: 4px;
75 | `;
76 |
77 | const LensStyle = {
78 | "width": "100%",
79 | "flex": "1 1 0",
80 | "overflow": "auto",
81 | "minHeight": "0px"
82 | };
83 |
84 | const LensHeader = styled.div`
85 | width: 100%;
86 | display: flex;
87 | flex-direction: row;
88 | justify-content: space-between;
89 |
90 | & > div:nth-child(2) {
91 | display: flex;
92 | flex-direction: row;
93 | gap: 4px;
94 | }
95 | `;
96 |
97 | const LensButtonMin = styled.div`
98 | height: 24px;
99 | width: 24px;
100 | border-radius: 4px;
101 | cursor: pointer;
102 | display: flex;
103 | justify-content: center;
104 | align-items: center;
105 | background-color: #fff;
106 | border: solid 2px #0088ff;
107 | fill: #0088ff;
108 |
109 | & > svg {
110 | height: 16px;
111 | width: 16px;
112 | }
113 | `;
114 |
115 |
116 | const CopywritingOuput = () => {
117 | const { lenses, resetLens, changeLensType } = React.useContext(ObjectsContext);
118 |
119 | const [text, setText] = React.useState("");
120 | const [textColor, setTextColor] = React.useState("#333333");
121 |
122 |
123 | // setTextColor("#0066ff");
124 | // setTimeout(() => {
125 | // setTextColor("#333333");
126 | // }, 500);
127 |
128 | const handleGenerationClick = (generationText: string) => {
129 | setText(text + generationText);
130 | setTextColor("#0066ff");
131 | setTimeout(() => {
132 | setTextColor("#333333");
133 | }, 500);
134 | }
135 |
136 | return (
137 |
138 |
139 |
141 |
142 |
143 |
144 |
145 | resetLens(lenses[0].id)}>
146 | {TrashSvg}
147 |
148 |
149 |
150 | {["list", "space"].map((type) => (
151 | changeLensType(lenses[0].id, type)}
153 | style={lenses[0].type !== type ? {borderColor: "#ccc", fill: "#ccc"} : {}}
154 | >
155 | {type === "list" ? ListSvg : SpaceSvg}
156 |
157 | ))}
158 |
159 |
160 |
167 |
168 |
169 |
176 |
177 |
178 |
179 | )
180 | }
181 |
182 | export default CopywritingOuput;
--------------------------------------------------------------------------------
/demo/src/components/Storywriting.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { ObjectsContextProvider, CellProps, GeneratorProps, ParameterProps, LensProps } from "llm-ui-objects";
4 |
5 | import { generate } from "./Api"
6 | import StorywritingInput from "./StorywritingInput";
7 | import StorywritingOutput from "./StorywritingOutput";
8 |
9 | const defaultCells: CellProps[] = [
10 | {
11 | id: "cell-0",
12 | text: "As soon as I entered the hall for the UIST conference, I noticed that something was missing. ",
13 | isActive: true,
14 | isMinimized: true,
15 | minimizedText: "UIST",
16 | parentCellId: null
17 | },
18 | {
19 | id: "cell-1",
20 | text: " A worried murmur spread through the crowd as we realized that the stage was indeed empty.",
21 | isActive: true,
22 | isMinimized: true,
23 | minimizedText: "murmur",
24 | parentCellId: "cell-0"
25 | },
26 | {
27 | id: "cell-2",
28 | text: " The usual buzz of excitement and chatter that filled the air was conspicuously absent.",
29 | isActive: false,
30 | isMinimized: true,
31 | minimizedText: "buzz",
32 | parentCellId: "cell-0"
33 | }
34 | ];
35 |
36 | const defaultParameters: ParameterProps[] = [
37 | {
38 | id: "model",
39 | name: "Model",
40 | value: "gpt-3.5-turbo",
41 | type: "nominal",
42 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "gpt-3.5-turbo-0301", "gpt-4-0314"],
43 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "gpt-3.5-turbo-0301": "3.5M", "gpt-4-0314": "4M"},
44 | defaultValue: "gpt-3.5-turbo"
45 | },
46 | {
47 | id: "temperature",
48 | name: "Temperature",
49 | nickname: "temp",
50 | value: 0.7,
51 | type: "continuous",
52 | allowedValues: [0.0, 2.0],
53 | defaultValue: 1.0
54 | },
55 | {
56 | id: "presence_penalty",
57 | name: "Presence Penalty",
58 | nickname: "present",
59 | value: 0.0,
60 | type: "continuous",
61 | allowedValues: [0.0, 2.0],
62 | defaultValue: 0.0
63 | },
64 | {
65 | id: "frequency_penalty",
66 | name: "Frequency Penalty",
67 | nickname: "frequent",
68 | value: 0.0,
69 | type: "continuous",
70 | allowedValues: [0.0, 2.0],
71 | defaultValue: 0.0
72 | }
73 | ];
74 |
75 | const defaultGenerators: GeneratorProps[] = [
76 | {
77 | id: "generator-0",
78 | parameters: JSON.parse(JSON.stringify(defaultParameters)),
79 | color: "#0088ff",
80 | size: "medium",
81 | isGenerating: false,
82 | isSelected: false,
83 | cellId: null,
84 | lensId: "lens-0"
85 | },
86 | {
87 | id: "generator-1",
88 | parameters: [
89 | {
90 | id: "model",
91 | name: "Model",
92 | value: "gpt-4",
93 | type: "nominal",
94 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "gpt-3.5-turbo-0301", "gpt-4-0314"],
95 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "gpt-3.5-turbo-0301": "3.5M", "gpt-4-0314": "4M"},
96 | defaultValue: "gpt-3.5-turbo"
97 | },
98 | {
99 | id: "temperature",
100 | name: "Temperature",
101 | nickname: "temp",
102 | value: 0.7,
103 | type: "continuous",
104 | allowedValues: [0.0, 2.0],
105 | defaultValue: 1.0
106 | },
107 | {
108 | id: "presence_penalty",
109 | name: "Presence Penalty",
110 | nickname: "present",
111 | value: 0.0,
112 | type: "continuous",
113 | allowedValues: [0.0, 2.0],
114 | defaultValue: 0.0
115 | },
116 | {
117 | id: "frequency_penalty",
118 | name: "Frequency Penalty",
119 | nickname: "frequent",
120 | value: 0.0,
121 | type: "continuous",
122 | allowedValues: [0.0, 2.0],
123 | defaultValue: 0.0
124 | }
125 | ],
126 | color: "#0088ff",
127 | size: "medium",
128 | isGenerating: false,
129 | isSelected: false,
130 | cellId: null,
131 | lensId: "lens-0"
132 | }
133 | ];
134 |
135 | const defaultLens: LensProps[] = [
136 | {
137 | id: "lens-0",
138 | type: "list",
139 | group: 0
140 | }
141 | ];
142 |
143 | const generateStory = (input: string | string[], parameters: any) => {
144 | const systemPrompt = "You are a creative writing assistant. You will be given a couple of sentences for a story, and you should generate a sentence that continues the story.";
145 | return generate(systemPrompt, input, parameters);
146 | }
147 |
148 | const Container = styled.div`
149 | height: 100%;
150 | display: flex;
151 | flex-direction: row;
152 | gap: 8px;
153 | `;
154 |
155 | const Storywriting = ({display}: {display: boolean}) => {
156 | return (
157 | Promise}
162 | minimizeHandler={(text: string) => {
163 | const words = text.split(" ").map((word) => word.replace(/[.,?\/#!$%\^&\*;:{}=\-_`~()]/g,""));
164 | const longestWord = words.reduce((a, b) => a.length > b.length ? a : b);
165 | return longestWord;
166 | }}
167 | >
168 |
169 |
170 |
171 |
172 |
173 | )
174 | }
175 |
176 | export default Storywriting;
--------------------------------------------------------------------------------
/demo/src/components/StorywritingInput.tsx:
--------------------------------------------------------------------------------
1 |
2 | import React from "react";
3 | import styled from "styled-components";
4 |
5 | import { ObjectsContext, CellTree, CellEditor } from "llm-ui-objects";
6 |
7 | const Container = styled.div`
8 | display: flex;
9 | flex-direction: row;
10 | gap: 8px;
11 | height: 100%;
12 | width: 55%;
13 | `;
14 |
15 | const StorywritingInput = () => {
16 | const { cells } = React.useContext(ObjectsContext);
17 |
18 | const activePath = cells.filter((cell) => cell.isActive).map((cell) => cell.id);
19 | const hoveredPath = cells.filter((cell) => cell.isHovered).map((cell) => cell.id);
20 |
21 | return (
22 |
23 | 0 ? hoveredPath : activePath}
25 | style={{width: "50%", backgroundColor: "#fff"}}
26 | textColor={hoveredPath.length > 0 ? "#0066ff99" : undefined}
27 | />
28 |
33 |
34 | )
35 | }
36 |
37 | export default StorywritingInput;
--------------------------------------------------------------------------------
/demo/src/components/StorywritingOutput.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { ObjectsContext, ParameterProps, Generator, GeneratorProps, Lens } from "llm-ui-objects";
4 |
5 | import { getPositions, getRatings } from "./Api";
6 |
7 | const PlusSvg = ;
8 | const ListSvg = ;
9 | const SpaceSvg = ;
10 | const PlotSvg = ;
11 | const TrashSvg = ;
12 |
13 | const Container = styled.div`
14 | display: flex;
15 | flex-direction: column;
16 | height: 100%;
17 | gap: 8px;
18 | flex: 1;
19 | position: relative;
20 | `;
21 |
22 | const RowContainer = styled.div`
23 | display: flex;
24 | flex-direction: row;
25 | flex: 1;
26 | `;
27 |
28 | const GeneratorContainer = styled.div`
29 | display: flex;
30 | flex-direction: column;
31 | gap: 8px;
32 | user-select: none;
33 | justify-content: center;
34 | `;
35 |
36 | const LensContainer = styled.div`
37 | flex: 1;
38 | border: solid 2px #0088ff;
39 | border-radius: 8px;
40 | box-shadow: 0px 4px 4px 2px rgba(0, 0, 0, 0.2);
41 | padding: 8px;
42 | background-color: #fff;
43 | box-sizing: border-box;
44 | display: flex;
45 | flex-direction: column;
46 | gap: 4px;
47 | `;
48 |
49 | const LinkContainer = styled.div`
50 | display: flex;
51 | flex-direction: column;
52 | justify-content: center;
53 | gap: 136px;
54 | width: 32px;
55 | user-select: none;
56 |
57 | & > div {
58 | width: 100%;
59 | height: 4px;
60 | background-color: #0088ff99;
61 | }
62 | `;
63 |
64 | const AddButton = styled.div`
65 | align-items: center;
66 | background-color: #0066ff;
67 | fill: #fff;
68 | border-radius: 4px;
69 | cursor: pointer;
70 | display: flex;
71 | justify-content: center;
72 | height: 40px;
73 | width: 40px;
74 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.2);
75 | `;
76 |
77 | const LensButton = styled.div`
78 | height: 48px;
79 | width: 48px;
80 | border-radius: 8px;
81 | cursor: pointer;
82 | box-shadow: 0 2px 2px 0 rgba(0,0,0,0.2);
83 | display: flex;
84 | justify-content: center;
85 | align-items: center;
86 | background-color: #fff;
87 | border: solid 2px #0088ff;
88 | fill: #0088ff;
89 |
90 | & > svg {
91 | height: 28px;
92 | width: 28px;
93 | }
94 | `;
95 |
96 | const LensStyle = {
97 | "width": "100%",
98 | "flex": "1 1 0",
99 | "overflow": "auto",
100 | "minHeight": "0px"
101 | };
102 |
103 | const LensHeader = styled.div`
104 | width: 100%;
105 | display: flex;
106 | flex-direction: row;
107 | justify-content: space-between;
108 |
109 | & > div:nth-child(2) {
110 | display: flex;
111 | flex-direction: row;
112 | gap: 4px;
113 | }
114 | `;
115 |
116 | const LensButtonMin = styled.div`
117 | height: 24px;
118 | width: 24px;
119 | border-radius: 4px;
120 | cursor: pointer;
121 | display: flex;
122 | justify-content: center;
123 | align-items: center;
124 | background-color: #fff;
125 | border: solid 2px #0088ff;
126 | fill: #0088ff;
127 |
128 | & > svg {
129 | height: 16px;
130 | width: 16px;
131 | }
132 | `;
133 |
134 |
135 | const StorywritingOutput = ({ parameters }: {parameters: ParameterProps[]}) => {
136 | const {
137 | cells,
138 | addCell,
139 | generators,
140 | addGenerator,
141 | linkCellToGenerator,
142 | lenses,
143 | addLens,
144 | removeLens,
145 | linkGeneratorToLens,
146 | changeLensType,
147 | resetLens
148 | } = React.useContext(ObjectsContext);
149 |
150 | const [ connector, setConnector ] = React.useState<{x1: number, y1: number, x2: number, y2: number, id: string} | null>(null);
151 | const [ leafCell, setLeafCell ] = React.useState(null);
152 |
153 | React.useEffect(() => {
154 | const activeCells = cells.filter((cell) => cell.isActive);
155 | const parentIds = activeCells.map((cell) => cell.parentCellId);
156 | const leafIds = activeCells.filter((cell) => !parentIds.includes(cell.id as string));
157 | if(leafIds.length === 0) return;
158 | const leafId = leafIds[0].id;
159 |
160 | generators.forEach((generator) => {
161 | if(generator.cellId === leafId) return;
162 | linkCellToGenerator(leafId, generator.id);
163 | });
164 |
165 | setLeafCell(leafId);
166 | }, [cells]);
167 |
168 | React.useEffect(() => {
169 | const lensWithNoGenerators = lenses.filter((lens) => {
170 | const linkedGenerators = generators.filter((generator) => generator.lensId === lens.id);
171 | return linkedGenerators.length === 0;
172 | });
173 |
174 | lensWithNoGenerators.forEach((lens) => {
175 | removeLens(lens.id);
176 | });
177 | }, [generators]);
178 |
179 | const handleGenerationClick = (generationText: string) => {
180 | addCell(
181 | " " + generationText,
182 | { parentCellId: leafCell }
183 | );
184 | }
185 |
186 | const handleAddGenerator = () => {
187 | addGenerator({
188 | id: "placeholder",
189 | color: "#0088ff",
190 | size: "medium",
191 | parameters: JSON.parse(JSON.stringify(parameters)),
192 | cellId: leafCell,
193 | lensId: null
194 | });
195 | }
196 |
197 | const handleAddLens = (generatorId: string, type: "list" | "space" | "plot") => {
198 | const lensId = addLens({
199 | id: "placeholder",
200 | type: type,
201 | group: -1,
202 | getGenerationMetadata: type === "space" ? getPositions : undefined
203 | });
204 | linkGeneratorToLens(generatorId, lensId);
205 | }
206 |
207 | const unlinkedGenerators = generators.filter((generator) => generator.lensId === null);
208 | const generatorIds = generators.map((generator) => generator.id);
209 |
210 | return (
211 | {
214 | const targetId = e.target.getAttribute("id");
215 | const isGenerator = generatorIds.includes(targetId);
216 | if(!isGenerator) return;
217 | const rect = document.getElementById("storywriting-output-container")?.getBoundingClientRect();
218 | if(!rect) return;
219 | const x = e.clientX - rect.left;
220 | const y = e.clientY - rect.top;
221 | setConnector({x1: x, y1: y, x2: x, y2: y, id: targetId});
222 | }}
223 | onMouseMove={(e: any) => {
224 | if(!connector) return;
225 | const rect = document.getElementById("storywriting-output-container")?.getBoundingClientRect();
226 | if(!rect) return;
227 | const x = e.clientX - rect.left;
228 | const y = e.clientY - rect.top;
229 | setConnector({...connector, x2: x, y2: y});
230 | }}
231 | onMouseUp={(e: any) => {
232 | if(!connector) return;
233 | setConnector(null);
234 |
235 | var targetId: string | null = null;
236 | for(var i = 0; i < lenses.length; i++) {
237 | const lens = lenses[i];
238 | const lensDiv = document.getElementById(lens.id);
239 | if(!lensDiv) return;
240 | const rect = lensDiv.getBoundingClientRect();
241 | if(e.clientX > rect.left && e.clientX < rect.right && e.clientY > rect.top && e.clientY < rect.bottom) {
242 | targetId = lens.id;
243 | break;
244 | }
245 | }
246 |
247 | if(!targetId) return;
248 |
249 | const linkedGenerators = generators.filter((generator) => generator.lensId === targetId);
250 | if(linkedGenerators.length === 2) return;
251 | linkGeneratorToLens(connector.id, targetId);
252 |
253 | const prevLinkedLens = generators.find((generator) => generator.id === connector.id)?.lensId;
254 | const alreadyLinkedGenerators = generators.filter((generator) => generator.lensId === prevLinkedLens);
255 | if(prevLinkedLens && alreadyLinkedGenerators.length === 1) removeLens(prevLinkedLens);
256 | }}
257 | >
258 |
259 | {connector && (
260 |
267 | )}
268 |
269 | {lenses.map((lens) => {
270 | const filteredGenerators = generators.filter((generator) => generator.lensId === lens.id);
271 | return (
272 |
273 |
274 | {filteredGenerators.map((generator) => (
275 |
279 | ))}
280 |
281 |
282 |
283 | {filteredGenerators.length === 2 && (
)}
284 |
285 |
286 |
287 |
288 | resetLens(lens.id)}>
289 | {TrashSvg}
290 |
291 |
292 |
293 | {["list", "space", "plot"].map((type) => (
294 | changeLensType(lens.id, type)}
296 | style={lens.type !== type ? {borderColor: "#ccc", fill: "#ccc"} : {}}
297 | >
298 | {type === "list" ? ListSvg : (type === "space" ? SpaceSvg : PlotSvg)}
299 |
300 | ))}
301 |
302 |
303 |
310 |
311 |
312 | );
313 | })}
314 | {unlinkedGenerators.map((generator) => (
315 |
316 |
317 |
321 |
322 |
323 |
324 | handleAddLens(generator.id, "list")}>
325 | {ListSvg}
326 |
327 | handleAddLens(generator.id, "space")}>
328 | {SpaceSvg}
329 |
330 | handleAddLens(generator.id, "plot")}>
331 | {PlotSvg}
332 |
333 |
334 |
335 | ))}
336 | {(lenses.length + unlinkedGenerators.length) < 3 && (
337 |
338 |
339 | {PlusSvg}
340 |
341 |
342 | )}
343 |
344 | )
345 |
346 | // return (
347 | //
348 | //
352 | // generator.id)}/>
353 | //
354 | // )
355 | }
356 |
357 | export default StorywritingOutput;
--------------------------------------------------------------------------------
/demo/src/index.css:
--------------------------------------------------------------------------------
1 | html {
2 | height: 100%;
3 | width: 100%;
4 | }
5 |
6 | body {
7 | margin: 0;
8 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
9 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
10 | sans-serif;
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: grayscale;
13 | height: 100%;
14 | width: 100%;
15 | }
16 |
17 | #root {
18 | height: 100%;
19 | width: 100%;
20 | }
21 |
22 | code {
23 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
24 | monospace;
25 | }
26 |
27 | .quill {
28 | height: 100%;
29 | }
30 |
31 | .ql-editor {
32 | font-size: 16px;
33 | }
34 |
35 | textarea {
36 | width: 100%;
37 | }
--------------------------------------------------------------------------------
/demo/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById('root') as HTMLElement
9 | );
10 | root.render(
11 |
12 |
13 |
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/demo/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/demo/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/demo/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/demo/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * For a detailed explanation regarding each configuration property, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | /** @type {import('jest').Config} */
7 | const config = {
8 | // All imported modules in your tests should be mocked automatically
9 | // automock: false,
10 |
11 | // Stop running tests after `n` failures
12 | // bail: 0,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "/private/var/folders/t7/6qpv_bwd6hvg74637hc2y2hm0000gn/T/jest_dx",
16 |
17 | // Automatically clear mock calls, instances, contexts and results before every test
18 | // clearMocks: false,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | // collectCoverage: false,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | // collectCoverageFrom: undefined,
25 |
26 | // The directory where Jest should output its coverage files
27 | // coverageDirectory: undefined,
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | // coveragePathIgnorePatterns: [
31 | // "/node_modules/"
32 | // ],
33 |
34 | // Indicates which provider should be used to instrument code for coverage
35 | // coverageProvider: "babel",
36 |
37 | // A list of reporter names that Jest uses when writing coverage reports
38 | // coverageReporters: [
39 | // "json",
40 | // "text",
41 | // "lcov",
42 | // "clover"
43 | // ],
44 |
45 | // An object that configures minimum threshold enforcement for coverage results
46 | // coverageThreshold: undefined,
47 |
48 | // A path to a custom dependency extractor
49 | // dependencyExtractor: undefined,
50 |
51 | // Make calling deprecated APIs throw helpful error messages
52 | // errorOnDeprecated: false,
53 |
54 | // The default configuration for fake timers
55 | // fakeTimers: {
56 | // "enableGlobally": false
57 | // },
58 |
59 | // Force coverage collection from ignored files using an array of glob patterns
60 | // forceCoverageMatch: [],
61 |
62 | // A path to a module which exports an async function that is triggered once before all test suites
63 | // globalSetup: undefined,
64 |
65 | // A path to a module which exports an async function that is triggered once after all test suites
66 | // globalTeardown: undefined,
67 |
68 | // A set of global variables that need to be available in all test environments
69 | // globals: {},
70 |
71 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
72 | // maxWorkers: "50%",
73 |
74 | // An array of directory names to be searched recursively up from the requiring module's location
75 | // moduleDirectories: [
76 | // "node_modules"
77 | // ],
78 |
79 | // An array of file extensions your modules use
80 | // moduleFileExtensions: [
81 | // "js",
82 | // "mjs",
83 | // "cjs",
84 | // "jsx",
85 | // "ts",
86 | // "tsx",
87 | // "json",
88 | // "node"
89 | // ],
90 |
91 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
92 | // moduleNameMapper: {},
93 |
94 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
95 | // modulePathIgnorePatterns: [],
96 |
97 | // Activates notifications for test results
98 | // notify: false,
99 |
100 | // An enum that specifies notification mode. Requires { notify: true }
101 | // notifyMode: "failure-change",
102 |
103 | // A preset that is used as a base for Jest's configuration
104 | // preset: undefined,
105 |
106 | // Run tests from one or more projects
107 | // projects: undefined,
108 |
109 | // Use this configuration option to add custom reporters to Jest
110 | // reporters: undefined,
111 |
112 | // Automatically reset mock state before every test
113 | // resetMocks: false,
114 |
115 | // Reset the module registry before running each individual test
116 | // resetModules: false,
117 |
118 | // A path to a custom resolver
119 | // resolver: undefined,
120 |
121 | // Automatically restore mock state and implementation before every test
122 | // restoreMocks: false,
123 |
124 | // The root directory that Jest should scan for tests and modules within
125 | // rootDir: undefined,
126 |
127 | // A list of paths to directories that Jest should use to search for files in
128 | // roots: [
129 | // ""
130 | // ],
131 |
132 | // Allows you to use a custom runner instead of Jest's default test runner
133 | // runner: "jest-runner",
134 |
135 | // The paths to modules that run some code to configure or set up the testing environment before each test
136 | // setupFiles: [],
137 |
138 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
139 | // setupFilesAfterEnv: [],
140 |
141 | // The number of seconds after which a test is considered as slow and reported as such in the results.
142 | // slowTestThreshold: 5,
143 |
144 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
145 | // snapshotSerializers: [],
146 |
147 | // The test environment that will be used for testing
148 | testEnvironment: "jsdom",
149 |
150 | // Options that will be passed to the testEnvironment
151 | // testEnvironmentOptions: {},
152 |
153 | // Adds a location field to test results
154 | // testLocationInResults: false,
155 |
156 | // The glob patterns Jest uses to detect test files
157 | // testMatch: [
158 | // "**/__tests__/**/*.[jt]s?(x)",
159 | // "**/?(*.)+(spec|test).[tj]s?(x)"
160 | // ],
161 |
162 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
163 | // testPathIgnorePatterns: [
164 | // "/node_modules/"
165 | // ],
166 |
167 | // The regexp pattern or array of patterns that Jest uses to detect test files
168 | // testRegex: [],
169 |
170 | // This option allows the use of a custom results processor
171 | // testResultsProcessor: undefined,
172 |
173 | // This option allows use of a custom test runner
174 | // testRunner: "jest-circus/runner",
175 |
176 | // A map from regular expressions to paths to transformers
177 | // transform: undefined,
178 |
179 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
180 | // transformIgnorePatterns: [
181 | // "/node_modules/",
182 | // "\\.pnp\\.[^\\/]+$"
183 | // ],
184 |
185 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
186 | // unmockedModulePathPatterns: undefined,
187 |
188 | // Indicates whether each individual test should be reported during the run
189 | // verbose: undefined,
190 |
191 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
192 | // watchPathIgnorePatterns: [],
193 |
194 | // Whether to use watchman for file crawling
195 | // watchman: true,
196 | };
197 |
198 | module.exports = config;
199 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "llm-ui-objects",
3 | "version": "1.3.0",
4 | "description": "Interactive object components for LLM-powered writing interfaces",
5 | "main": "dist/cjs/index.js",
6 | "module": "dist/esm/index.js",
7 | "types": "dist/types.d.ts",
8 | "scripts": {
9 | "test": "jest",
10 | "build": "rollup -c --bundleConfigAsCjs",
11 | "storybook": "storybook dev -p 6006",
12 | "build-storybook": "storybook build"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/kixlab/llm-ui-objects.git"
17 | },
18 | "keywords": [
19 | "LLM",
20 | "AI",
21 | "NLP",
22 | "Writing"
23 | ],
24 | "author": "Tae Soo Kim",
25 | "license": "ISC",
26 | "bugs": {
27 | "url": "https://github.com/kixlab/llm-ui-objects/issues"
28 | },
29 | "homepage": "https://github.com/kixlab/llm-ui-objects#readme",
30 | "devDependencies": {
31 | "@rollup/plugin-commonjs": "^25.0.5",
32 | "@rollup/plugin-node-resolve": "^15.2.3",
33 | "@rollup/plugin-terser": "^0.4.4",
34 | "@rollup/plugin-typescript": "^11.1.5",
35 | "@storybook/addon-essentials": "^7.4.5",
36 | "@storybook/addon-interactions": "^7.4.5",
37 | "@storybook/addon-links": "^7.4.5",
38 | "@storybook/blocks": "^7.4.5",
39 | "@storybook/react": "^7.4.5",
40 | "@storybook/react-webpack5": "^7.4.5",
41 | "@storybook/testing-library": "^0.0.14-next.2",
42 | "@testing-library/jest-dom": "^6.1.3",
43 | "@testing-library/react": "^14.0.0",
44 | "@testing-library/user-event": "^14.5.1",
45 | "@types/jest": "^29.5.5",
46 | "@types/styled-components": "^5.1.28",
47 | "jest": "^29.7.0",
48 | "jest-environment-jsdom": "^29.7.0",
49 | "rollup": "^3.29.3",
50 | "rollup-plugin-dts": "^6.1.0",
51 | "rollup-plugin-peer-deps-external": "^2.2.4",
52 | "storybook": "^7.4.5",
53 | "typescript": "^5.2.2"
54 | },
55 | "peerDependencies": {
56 | "@types/react": "^18.2.28",
57 | "@types/react-dom": "^18.2.13",
58 | "react": "^18.2.0",
59 | "react-dom": "^18.2.0"
60 | },
61 | "dependencies": {
62 | "prop-types": "^15.8.1",
63 | "react-quill": "^2.0.0",
64 | "styled-components": "^6.0.8"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import resolve from "@rollup/plugin-node-resolve";
2 | import commonjs from "@rollup/plugin-commonjs";
3 | import typescript from "@rollup/plugin-typescript";
4 | import dts from "rollup-plugin-dts";
5 | import terser from "@rollup/plugin-terser";
6 | import peerDepsExternal from "rollup-plugin-peer-deps-external";
7 |
8 | const packageJson = require("./package.json");
9 |
10 | export default [
11 | {
12 | input: "src/index.ts",
13 | output: [
14 | {
15 | file: packageJson.main,
16 | format: "cjs",
17 | sourcemap: true,
18 | },
19 | {
20 | file: packageJson.module,
21 | format: "esm",
22 | sourcemap: true,
23 | },
24 | ],
25 | plugins: [
26 | peerDepsExternal(),
27 | resolve(),
28 | commonjs(),
29 | typescript({ tsconfig: "./tsconfig.json" }),
30 | terser()
31 | ],
32 | external: ["react", "react-dom", "styled-components", "react-quill"],
33 | },
34 | {
35 | input: "src/index.ts",
36 | output: [{ file: "dist/types.d.ts", format: "es" }],
37 | plugins: [dts.default()],
38 | },
39 | ];
--------------------------------------------------------------------------------
/src/components/Cell/Cell.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 | import Cell from "./Cell";
4 |
5 | const meta: Meta = {
6 | component: Cell,
7 | title: "tsook/Cell",
8 | argTypes: {},
9 | };
10 | export default meta;
11 |
12 | type Story = StoryObj;
13 |
14 | export const Primary: Story = (args) => (
15 | |
16 | );
17 | Primary.args = {
18 | id: "Cell-id",
19 | text: "Cell\ntext"
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/src/components/Cell/Cell.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import '@testing-library/jest-dom';
3 | import userEvent from "@testing-library/user-event";
4 | import {render, screen, waitFor } from '@testing-library/react';
5 |
6 | import Cell from "./Cell";
7 |
8 | describe("Cells", () => {
9 | var mockCell = {
10 | id: "Cell-id",
11 | text: "Cell text",
12 | isActive: false,
13 | isMinimized: false,
14 | isSelected: false,
15 | parentCellId: null
16 | };
17 |
18 | test("renders Generator component", () => {
19 | render( | );
20 |
21 | const textArea = screen.getByRole("textbox");
22 | userEvent.type(textArea, "Cell text");
23 | expect(textArea).toHaveValue("Cell text");
24 | });
25 | });
--------------------------------------------------------------------------------
/src/components/Cell/Cell.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { CellProps } from "./Cell.types";
4 | import { ObjectsContext } from "../../context/ObjectsContextProvider";
5 |
6 | const CellContainer = styled.div<{ active: number, selected: number, hovered: number, tabDirection?: string}>`
7 | display: flex;
8 | border: solid 2px ${({ active, selected }) => selected ? "rgb(0, 194, 255)" : (active ? '#0066ff' : "#ddd")};
9 | border-${({ tabDirection }) => tabDirection ? tabDirection : "top"}-width: 12px;
10 | border-radius: 8px;
11 | flex: 1;
12 | cursor: pointer;
13 | `;
14 |
15 | const MinCellContainer = styled.div<{ active: number, hovered: number, selected: number}>`
16 | display: flex;
17 | border: solid 2px ${({ active, selected, hovered }) => selected ? "rgb(0, 194, 255)" : (active ? '#0066ff' : (hovered ? "#0066ff66" : "#aaa"))};
18 | background-color: ${({ active, selected, hovered }) => selected ? "rgb(0, 194, 255)" : (active ? '#0066ff' : (hovered ? "#0066ff22" : "#fff"))};
19 | color: ${({ active }) => active ? "#fff" : "#999"};
20 | border-radius: 8px;
21 | cursor: pointer;
22 | overflow: hidden;
23 | white-space: nowrap;
24 | text-overflow: ellipsis;
25 | justify-content: center;
26 | align-items: center;
27 | box-sizing: border-box;
28 | -webkit-user-select: none;
29 | -ms-user-select: none;
30 | user-select: none;
31 | `;
32 |
33 | const CellContent = styled.textarea`
34 | font-family: inherit;
35 | font-size: 14px;
36 | color: #555;
37 | resize: none;
38 | padding: 4px 8px;
39 | margin: 0;
40 | border: none;
41 | border-radius: 8px;
42 | flex: 1;
43 |
44 | &:focus {
45 | outline: none;
46 | }
47 | `;
48 |
49 | const Cell: React.FC = ({
50 | id,
51 | text,
52 | isActive,
53 | isMinimized,
54 | isSelected,
55 | isHovered,
56 | tabDirection,
57 | minimizedText,
58 | onClick,
59 | onMouseEnter,
60 | onMouseLeave,
61 | style
62 | }) => {
63 | const { updateCell, toggleCell } = React.useContext(ObjectsContext);
64 | const [currText, setCurrText] = React.useState(text);
65 |
66 | // resize textarea to fit content
67 | React.useEffect(() => {
68 | const textarea = document.getElementById(id + '-textarea') as HTMLTextAreaElement;
69 | if (textarea) {
70 | textarea.style.height = "auto";
71 | textarea.style.height = textarea.scrollHeight - 8 + "px";
72 | }
73 | }, [currText]);
74 |
75 | const handleClick = (e: React.MouseEvent) => {
76 | e.stopPropagation();
77 | if(e.detail === 1) {
78 | if(isSelected) toggleCell(id, 'isSelected');
79 | if (onClick) onClick(e);
80 | else toggleCell(id, 'isActive');
81 | } else if(e.detail === 2) {
82 | toggleCell(id, 'isSelected');
83 | }
84 | }
85 |
86 | const handleMouseEnter = (e: React.MouseEvent) => {
87 | if(onMouseEnter) onMouseEnter(e);
88 | toggleCell(id, 'isHovered');
89 | }
90 | const handleMouseLeave = (e: React.MouseEvent) => {
91 | if(onMouseLeave) onMouseLeave(e);
92 | toggleCell(id, 'isHovered');
93 | }
94 |
95 | if(!isMinimized) {
96 | return (
97 |
110 | {
114 | updateCell(id, e.target.value)
115 | setCurrText(e.target.value)
116 | }}
117 | />
118 |
119 | )
120 | } else {
121 | return (
122 |
134 | {minimizedText}
135 |
136 | )
137 | }
138 | }
139 |
140 | export default Cell;
--------------------------------------------------------------------------------
/src/components/Cell/Cell.types.ts:
--------------------------------------------------------------------------------
1 | export interface CellProps {
2 | id: string;
3 | text: string;
4 | isActive: boolean;
5 | isMinimized?: boolean;
6 | isSelected?: boolean;
7 | isHovered?: boolean;
8 | minimizedText?: string;
9 | tabDirection?: string;
10 | onClick?: (e: React.MouseEvent) => void;
11 | onMouseEnter?: (e: React.MouseEvent) => void;
12 | onMouseLeave?: (e: React.MouseEvent) => void;
13 | parentCellId: string | null;
14 | style?: React.CSSProperties;
15 | }
--------------------------------------------------------------------------------
/src/components/Cell/CellBoard.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 | import CellBoard from "./CellBoard";
4 | import { ObjectsContextProvider } from "../../context/ObjectsContextProvider";
5 |
6 | const meta: Meta = {
7 | component: CellBoard,
8 | title: "tsook/CellBoard",
9 | argTypes: {},
10 | };
11 | export default meta;
12 |
13 | //type of cell or cellboard
14 | type Story = StoryObj;
15 |
16 | const generateHandler = (input: string | string[], parameters: any) => {
17 | return Promise.resolve(input);
18 | }
19 |
20 | export const Primary: Story = (args) => (
21 |
22 |
23 |
24 | );
25 | Primary.args = {
26 | initialBoard: [
27 | ["Cell 1", "Cell 2", "Cell 3"],
28 | ["Cell 4", "Cell 5", "Cell 6"],
29 | ["Cell 7", "Cell 8"],
30 | ],
31 | maxRows: 5,
32 | maxColumns: 5,
33 | setEntryCell: (cellId: string | undefined) => { },
34 | }
--------------------------------------------------------------------------------
/src/components/Cell/CellBoard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import Cell from "./Cell";
4 | import { CellProps } from "./Cell.types";
5 | import { ObjectsContext } from "../../context/ObjectsContextProvider";
6 |
7 | const CellBoardContainer = styled.div`
8 | display: flex;
9 | flex-direction: column;
10 | gap: 8px;
11 | width: 100%;
12 | height: 100%;
13 | `;
14 |
15 | const CellRow = styled.div`
16 | display: flex;
17 | flex-direction: row;
18 | justify-content: center;
19 | align-items: center;
20 | gap: 8px;
21 | width: 100%;
22 | `;
23 |
24 | const CellsContainer = styled.div`
25 | display: flex;
26 | flex-direction: row;
27 | gap: 8px;
28 | flex-grow: 1;
29 | `;
30 |
31 | const CellRowControls = styled.div`
32 | display: flex;
33 | `;
34 |
35 | const AddColumnButton = styled.button`
36 | display: flex;
37 | justify-content: center;
38 | align-items: center;
39 | height: 32px;
40 | width: 32px;
41 | border-radius: 8px;
42 | background-color: #0088ff99;
43 | color: #ffffff;
44 | font-size: 24px;
45 | font-weight: bold;
46 | border: none;
47 | cursor: pointer;
48 | &:hover {
49 | background-color: #0088ff;
50 | }
51 | &:disabled {
52 | background-color: #dddddd;
53 | cursor: auto;
54 | }
55 | `;
56 |
57 | const AddRowButton = styled.button`
58 | display: flex;
59 | justify-content: center;
60 | align-items: center;
61 | height: 32px;
62 | width: 160px;
63 | border-radius: 8px;
64 | background-color: #0088ff99;
65 | color: #ffffff;
66 | font-size: 16px;
67 | border: none;
68 | cursor: pointer;
69 | &:hover {
70 | background-color: #0088ff;
71 | }
72 | &:disabled {
73 | background-color: #dddddd;
74 | cursor: auto;
75 | }
76 | `;
77 |
78 | interface CellBoardProps {
79 | initialBoard: string[][];
80 | maxRows: number;
81 | maxColumns: number;
82 | setEntryCell: (cellId: string | undefined) => void;
83 | style?: React.CSSProperties;
84 | }
85 |
86 | const CellBoard: React.FC = ({
87 | initialBoard,
88 | maxRows,
89 | maxColumns,
90 | setEntryCell,
91 | style
92 | }) => {
93 | const [board, setBoard] = React.useState([]); // 2d array of cell ids
94 | const [activeCells, setActiveCells] = React.useState<(string | undefined)[]>([]); // array of cell ids or undefined for each row
95 | const { cells, addCell, linkCells, unlinkCell, toggleCell } = React.useContext(ObjectsContext);
96 |
97 | // initialize board
98 | React.useEffect(() => {
99 | var newBoard = initialBoard.map(row => row.map(text => {
100 | const newCellId = addCell(text);
101 | return newCellId;
102 | }));
103 | if(initialBoard.length == 0) {
104 | const newCellId = addCell("");
105 | newBoard = [[newCellId]];
106 | }
107 | setBoard(newBoard);
108 | setActiveCells(new Array(newBoard.length).fill(undefined));
109 | }, []);
110 |
111 | const handleActivateCell = (cell: CellProps, rowIndex: number) => {
112 | const newActiveCells = [...activeCells];
113 | if(cell.isActive) {
114 | const parentCellId = cell.parentCellId;
115 | const childCellId = activeCells[rowIndex + 1];
116 | if(parentCellId && childCellId) {
117 | linkCells(childCellId, parentCellId);
118 | } else if(childCellId) {
119 | unlinkCell(childCellId);
120 | }
121 | unlinkCell(cell.id);
122 | newActiveCells[rowIndex] = undefined;
123 | } else {
124 | // if a cell is already active in row, deactivate it
125 | const activeCellId = activeCells[rowIndex];
126 | if(activeCellId) {
127 | toggleCell(activeCellId, 'isActive');
128 | unlinkCell(activeCellId);
129 | }
130 | // link cell to parent
131 | const parentCellId = activeCells[rowIndex - 1];
132 | const childCellId = activeCells[rowIndex + 1];
133 | if(parentCellId) {
134 | linkCells(cell.id, parentCellId);
135 | }
136 | if(childCellId) {
137 | linkCells(childCellId, cell.id);
138 | }
139 | newActiveCells[rowIndex] = cell.id;
140 | }
141 |
142 | // find last activecell that is not undefined
143 | const inputCell = newActiveCells.reduceRight((acc, cur) => {
144 | if(acc) return acc;
145 | if(cur) return cur;
146 | });
147 | setEntryCell(inputCell);
148 |
149 | // activate or deactivate cell
150 | toggleCell(cell.id, 'isActive');
151 | setActiveCells(newActiveCells);
152 | }
153 |
154 | const addCellToRow = (rowIndex: number) => {
155 | const newCellId = addCell("");
156 | const newBoard = [...board];
157 | if(rowIndex === newBoard.length) {
158 | newBoard.push([]);
159 | }
160 | newBoard[rowIndex].push(newCellId);
161 | setBoard(newBoard);
162 | }
163 |
164 | return (
165 |
166 | {board.map((row, rowIndex) => (
167 |
168 |
169 | {row.map((cellId, columnIndex) => {
170 | const cell = cells.find(cell => cell.id === cellId);
171 | if (!cell) return null;
172 | return (
173 | handleActivateCell(cell, rowIndex)}
177 | />
178 | );
179 | })}
180 | |
181 |
182 | addCellToRow(rowIndex)}
184 | disabled={row.length >= maxColumns}
185 | >
186 | +
187 |
188 |
189 |
190 | ))}
191 |
192 | addCellToRow(board.length)}
194 | disabled={board.length >= maxRows}
195 | >
196 | + New Row
197 |
198 |
199 |
200 | )
201 | };
202 |
203 | export default CellBoard;
--------------------------------------------------------------------------------
/src/components/Cell/CellEditor.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 | import CellEditor from "./CellEditor";
4 | import { ObjectsContextProvider } from "../../context/ObjectsContextProvider";
5 |
6 | const meta: Meta = {
7 | component: CellEditor,
8 | title: "tsook/CellEditor",
9 | argTypes: {},
10 | };
11 | export default meta;
12 |
13 | //type of cell or cellboard
14 | type Story = StoryObj;
15 |
16 | const generateHandler = (input: string | string[], parameters: any) => {
17 | return Promise.resolve(input);
18 | }
19 |
20 | export const Primary: Story = (args) => (
21 |
29 |
30 |
31 | );
32 | Primary.args = {
33 | cellIds: ["1", "2", "3"],
34 | }
--------------------------------------------------------------------------------
/src/components/Cell/CellEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import ReactQuill, { Quill } from "react-quill";
4 | import { DeltaStatic, Sources } from "quill";
5 |
6 | import { CellProps } from "./Cell.types";
7 | import { ObjectsContext } from "../../context/ObjectsContextProvider";
8 |
9 | const Container = styled.div<{ textColor?: string }>`
10 | ${({ textColor }) => textColor ? `& span { color: ${textColor} !important; }` : ""}
11 | `;
12 |
13 | interface CellEditorProps {
14 | cellIds: string[];
15 | style?: React.CSSProperties;
16 | textColor?: string;
17 | }
18 |
19 | let Inline = Quill.import('blots/inline');
20 | class CellBlot extends Inline {
21 | static create(value: any) {
22 | let node = super.create(value);
23 | node.setAttribute('id', value.id);
24 | node.setAttribute('style', value.style);
25 | return node;
26 | }
27 |
28 | static value(node: any) {
29 | return {
30 | id: node.getAttribute('id'),
31 | style: node.getAttribute('style')
32 | };
33 | }
34 |
35 | format(name: any, value: any) {
36 | if (name === CellBlot.blotName && value) {
37 | this.domNode.setAttribute('id', value.id);
38 | this.domNode.setAttribute('style', value.style);
39 | } else {
40 | super.format(name, value);
41 | }
42 | }
43 | }
44 | CellBlot.blotName = 'cell';
45 | CellBlot.tagName = 'span';
46 | Quill.register('formats/cell', CellBlot);
47 |
48 | const formats = [
49 | 'bold', 'italic', 'strike', 'underline',
50 | 'color', 'background', 'font', 'code',
51 | 'cell'
52 | ];
53 |
54 | const CellEditor: React.FC = ({
55 | cellIds,
56 | style,
57 | textColor
58 | }) => {
59 | const quillRef = React.useRef(null);
60 | const reactQuillRef = React.useRef(null);
61 |
62 | const { cells, updateCell, addCell, toggleCell } = React.useContext(ObjectsContext);
63 | const [value, setValue] = React.useState("");
64 |
65 | const prevCellIds = React.useRef(cellIds);
66 | const prevCells = React.useRef(cells);
67 |
68 | const attachQuillRefs = () => {
69 | if (typeof reactQuillRef.current?.getEditor !== 'function') return;
70 | const quill = reactQuillRef.current.getEditor();
71 | if (quill != null) quillRef.current = quill;
72 | }
73 |
74 | const getOrderedActiveCells = () => {
75 | var newActiveCells: CellProps[] = cellIds.map(cellId => {
76 | const cell = cells.find(cell => cell.id == cellId);
77 | return cell;
78 | }) as CellProps[];
79 |
80 | // reorder newActiveCells from parent to child
81 | const newActiveCellsOrdered: CellProps[] = [];
82 | var currParent = newActiveCells.find(cell => !cell.parentCellId);
83 | while(currParent) {
84 | newActiveCellsOrdered.push(currParent);
85 | currParent = newActiveCells.find(cell => cell.parentCellId == currParent?.id);
86 | }
87 |
88 | return newActiveCellsOrdered;
89 | }
90 |
91 | React.useEffect(() => {
92 | attachQuillRefs();
93 | initializeCells(getOrderedActiveCells());
94 | }, []);
95 |
96 | React.useEffect(() => {
97 | if(cellIds.length == prevCellIds.current.length && cellIds.every((value, index) => value == prevCellIds.current[index])) return;
98 |
99 | initializeCells(getOrderedActiveCells());
100 |
101 | prevCellIds.current = cellIds;
102 | }, [cellIds]);
103 |
104 | React.useEffect(() => {
105 | // check if any cell has been selected that was not previously selected
106 | const newSelectedCell = cells.find(cell => cell.isSelected);
107 | const prevSelectedCellId = prevCells.current.find(cell => cell.isSelected)?.id;
108 | const prevSelectedCell = cells.find(cell => cell.id == prevSelectedCellId);
109 |
110 | if(prevSelectedCell === newSelectedCell) return;
111 |
112 | var currIdx = 0;
113 | var prevIdx = -1;
114 | var newIdx = -1;
115 | const activeCells = getOrderedActiveCells();
116 | for(var i = 0; i < activeCells.length; i++) {
117 | if(activeCells[i].id == prevSelectedCellId) {
118 | prevIdx = currIdx;
119 | } else if(activeCells[i].id == newSelectedCell?.id) {
120 | newIdx = currIdx;
121 | }
122 | currIdx += activeCells[i].text.length;
123 | }
124 |
125 | if(prevSelectedCell) {
126 | // get selection in quill
127 | const length = prevSelectedCell.text.length;
128 | quillRef.current.formatText(prevIdx, length, 'background', false);
129 | }
130 | if(newSelectedCell) {
131 | // get selection in quill
132 | const length = newSelectedCell.text.length;
133 | quillRef.current.formatText(newIdx, length, 'background', "#0088ff33");
134 | }
135 |
136 | prevCells.current = cells;
137 | }, [cells]);
138 |
139 | const initializeCells = (cells: (CellProps | undefined)[]) => {
140 | const contents = cells.map((cell, index) => {
141 | if(!cell) return;
142 | return {
143 | insert: cell.text,
144 | attributes: {
145 | cell: { id: cell.id, style: index % 2 == 0 ? "color:#333" : "color:#666" }
146 | }
147 | };
148 | });
149 | quillRef.current.setContents(contents);
150 | }
151 |
152 | const parseSpans = (value: string) => {
153 | const spans = value.split("");
154 | const cells = spans.map(span => {
155 | const cell = span.split("")[1];
159 | return {id, text};
160 | });
161 | return cells;
162 | }
163 |
164 | const checkChangedCells = (prevValue: string, newValue: string) => {
165 | const prevCellText = parseSpans(prevValue) as {id: string, text: string}[];
166 | const newCellText = parseSpans(newValue) as {id: string, text: string}[];
167 |
168 | const updatedCells: {id: string, text: string}[] = [];
169 | const deletedCells: {id: string, text: string}[] = [];
170 |
171 | prevCellText.forEach(prevCell => {
172 | const newCell = newCellText.find(newCell => newCell.id == prevCell.id);
173 | if(!newCell) {
174 | deletedCells.push(prevCell as {id: string, text: string});
175 | } else if(newCell.text != prevCell.text) {
176 | updatedCells.push(newCell);
177 | }
178 | })
179 |
180 | updatedCells.forEach(cell => {
181 | updateCell(cell.id, cell.text);
182 | });
183 | deletedCells.forEach(cell => {
184 | updateCell(cell.id, "␡");
185 | });
186 |
187 | if(deletedCells.length > 0) {
188 | const activeCells = getOrderedActiveCells();
189 | const newCells = activeCells.map(cell => {
190 | if(deletedCells.find(deletedCell => deletedCell.id == cell.id)) {
191 | return {...cell, text: "␡"};
192 | }
193 | const updatedCell = updatedCells.find(updatedCell => updatedCell.id == cell.id);
194 | if(updatedCell) {
195 | return {...cell, text: updatedCell.text};
196 | }
197 | return cell;
198 | });
199 | initializeCells(newCells);
200 | } else {
201 | setValue(newValue);
202 | }
203 | }
204 |
205 | return (
206 | {
210 | e.stopPropagation();
211 | }}
212 | >
213 |
214 | {
219 | if(source != "user") {
220 | setValue(newValue);
221 | return;
222 | }
223 | checkChangedCells(value, newValue);
224 | }}
225 | onChangeSelection={(range: any, source: Sources, editor: any) => {
226 | if(!range || range.length > 0) return;
227 | if(range.index == 0) {
228 | toggleCell(cells[0].id, 'isSelected');
229 | return;
230 | }
231 | const [leaf] = quillRef.current.getLeaf(range.index);
232 | if(!leaf) return;
233 | const cellId = leaf.parent.domNode.getAttribute("id");
234 | if(!cellId) return;
235 | const cell = cells.find(cell => cell.id == cellId);
236 | if(!cell || cell.isSelected) return;
237 | toggleCell(cellId, 'isSelected');
238 | }}
239 | onKeyDown={(e: any) => {
240 | if(e.key == "Enter" && (e.metaKey || e.ctrlKey)) {
241 | e.preventDefault();
242 | const activeCells = getOrderedActiveCells();
243 | const parentId = activeCells[activeCells.length-1].id;
244 | addCell(" ", {parentCellId: parentId, isActive: true});
245 |
246 | setTimeout(() => {
247 | const length = quillRef.current.getLength();
248 | quillRef.current.setSelection(length, 0);
249 | }, 10);
250 | } else if(e.key == "Backspace" && (e.metaKey || e.ctrlKey)) {
251 | e.stopPropagation();
252 | }
253 | }}
254 | modules={{toolbar: false}}
255 | formats={formats}
256 | />
257 |
258 | )
259 | }
260 |
261 | export default CellEditor;
--------------------------------------------------------------------------------
/src/components/Cell/CellTree.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 | import CellTree from "./CellTree";
4 | import { ObjectsContextProvider } from "../../context/ObjectsContextProvider";
5 |
6 | const meta: Meta = {
7 | component: CellTree,
8 | title: "tsook/CellTree",
9 | argTypes: {},
10 | };
11 | export default meta;
12 |
13 | //type of cell or cellboard
14 | type Story = StoryObj;
15 |
16 | const generateHandler = (input: string | string[], parameters: any) => {
17 | return Promise.resolve(input);
18 | }
19 |
20 | export const Primary: Story = (args) => (
21 | None"},
24 | {id: "2", text: "this one linked to 1", parentCellId: "1", isActive: false, isMinimized: false, isSelected: false, minimizedText: "2->1"},
25 | {id: "3", text: "this one linked to 2", parentCellId: "2", isActive: false, isMinimized: false, isSelected: false, minimizedText: "3->2"},
26 | {id: "4", text: "This should be linked to 1", parentCellId: "1", isActive: false, isMinimized: false, isSelected: false, minimizedText: "4->1"},
27 | {id: "5", text: "This should be linked to 4", parentCellId: "4", isActive: false, isMinimized: false, isSelected: false, minimizedText: "5->4"},
28 | {id: "6", text: "This should be linked to nothing", parentCellId: null, isActive: false, isMinimized: false, isSelected: false, minimizedText: "6->None"},
29 | {id: "7", text: "This should be linked to 6", parentCellId: "6", isActive: false, isMinimized: false, isSelected: false, minimizedText: "7->6"},
30 | {id: "8", text: "This should be linked to 7", parentCellId: "7", isActive: false, isMinimized: false, isSelected: false, minimizedText: "8->7"},
31 | {id: "9", text: "This should be linked to 4", parentCellId: "4", isActive: false, isMinimized: false, isSelected: false, minimizedText: "9->4"},
32 | ]}
33 | generateHandler={generateHandler}
34 | >
35 |
36 |
37 | );
38 | Primary.args = {
39 | cellWidth: 108,
40 | cellHeight: 28
41 | }
--------------------------------------------------------------------------------
/src/components/Cell/CellTree.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import Cell from "./Cell";
4 | import { CellProps } from "./Cell.types";
5 | import { ObjectsContext } from "../../context/ObjectsContextProvider";
6 |
7 | const CellTreeContainer = styled.div<{ cellHeight: number }>`
8 | display: flex;
9 | flex-direction: column;
10 | gap: ${({cellHeight}) => cellHeight}px;
11 | `;
12 |
13 | const CellTreeRow = styled.div`
14 | display: flex;
15 | flex-direction: row;
16 | gap: 8px;
17 | width: 100%;
18 | `;
19 |
20 | interface TreeNode {
21 | cellId: string | null;
22 | children: TreeNode[];
23 | }
24 |
25 | interface CellTreeProps {
26 | cellWidth: number;
27 | cellHeight: number;
28 | style?: React.CSSProperties;
29 | }
30 |
31 | const CellTree: React.FC = ({
32 | cellWidth,
33 | cellHeight,
34 | style
35 | }) => {
36 | const { cells, toggleCell, linkCells } = React.useContext(ObjectsContext);
37 | const [ treeStructure, setTreeStructure ] = React.useState({cellId: null, children: []});
38 |
39 | const [ connector, setConnector ] = React.useState<{x1: number, y1: number, id: string, x2: number, y2: number} | null>(null);
40 |
41 | const recurseTreeStructure = (cells: CellProps[], currParent: string | null) => {
42 | const childrenCells: CellProps[] = cells.filter((cell: CellProps) => cell.parentCellId === currParent);
43 | const children: TreeNode[] = [];
44 | childrenCells.forEach((childCell: CellProps) => {
45 | children.push({
46 | cellId: childCell.id,
47 | children: recurseTreeStructure(cells, childCell.id)
48 | });
49 | });
50 | return children;
51 | }
52 |
53 | const parseTreeStructure = (cells: CellProps[]) => {
54 | return {
55 | cellId: null,
56 | children: recurseTreeStructure(cells, null)
57 | }
58 | }
59 |
60 | React.useEffect(() => {
61 | setTreeStructure(parseTreeStructure(cells));
62 | }, [cells]);
63 |
64 | const getCurrPath = (cellId: string) => {
65 | const path: string[] = [];
66 | let currentCell = cells.find((cell: CellProps) => cell.id === cellId);
67 | while(currentCell) {
68 | path.push(currentCell.id);
69 | currentCell = cells.find((cell: CellProps) => cell.id === currentCell?.parentCellId);
70 | }
71 | path.reverse();
72 | return path;
73 | }
74 |
75 | const selectPath = (cellId: string) => {
76 | const path = getCurrPath(cellId);
77 | cells.forEach((cell: CellProps) => {
78 | if(path.includes(cell.id) && !cell.isActive) {
79 | toggleCell(cell.id, 'isActive');
80 | } else if(!path.includes(cell.id) && cell.isActive) {
81 | toggleCell(cell.id, 'isActive');
82 | }
83 | });
84 | }
85 |
86 | const onHoverPath = (cellId: string) => {
87 | const path = getCurrPath(cellId);
88 | cells.forEach((cell: CellProps) => {
89 | if(path.includes(cell.id) && !cell.isHovered) {
90 | toggleCell(cell.id, 'isHovered');
91 | } else if(!path.includes(cell.id) && cell.isHovered) {
92 | toggleCell(cell.id, 'isHovered');
93 | }
94 | });
95 | }
96 |
97 | const rows: any[] = [];
98 | const edges: any[] = [];
99 | var current: (TreeNode & {parentPos?: number})[] = treeStructure.children;
100 | var jointChildren: (TreeNode & {parentPos?: number})[] = [];
101 |
102 | var depth = 0;
103 | while(current.length > 0) {
104 | const row: any[] = [];
105 | for(let i = 0; i < current.length; i++) {
106 | const cell = cells.find((cell: CellProps) => cell.id === current[i].cellId);
107 | if(cell) {
108 | row.push(
109 | {
114 | e.stopPropagation();
115 | selectPath(cell.id)
116 | }}
117 | style={{width: cellWidth+"px", height: cellHeight+"px"}}
118 | onMouseEnter={() => onHoverPath(cell.id)}
119 | onMouseLeave={() => onHoverPath("")}
120 | />
121 | );
122 | jointChildren = jointChildren.concat(current[i].children.map((child: TreeNode) => ({...child, parentPos: i})));
123 |
124 | const parentPos = current[i].parentPos;
125 | if(parentPos !== undefined) {
126 | const startPosition = {
127 | x: parentPos*(8+cellWidth) + (cellWidth / 2),
128 | y: 2*depth*cellHeight - cellHeight - 1
129 | }
130 | const endPosition = {
131 | x: i*(cellWidth+8) + (cellWidth / 2),
132 | y: (2 * depth * cellHeight) + 1
133 | }
134 | edges.push(
135 |
143 | )
144 | }
145 | }
146 | }
147 | rows.push(row);
148 | current = jointChildren;
149 | jointChildren = [];
150 | depth++;
151 | }
152 |
153 | return (
154 | {
158 | if(!e.target.className.includes("llmuiobj-cell-min")) return;
159 | const rect = document.getElementById("llmuiobj-tree-container")?.getBoundingClientRect();
160 | if(!rect) return;
161 | const id = e.target.getAttribute("id");
162 | const x = e.clientX - rect.left;
163 | const y = e.clientY - rect.top;
164 | setConnector({x1: x, y1: y, id: id, x2: x, y2: y});
165 | }}
166 | onMouseMove={(e: any) => {
167 | if(!connector) return;
168 | const rect = document.getElementById("llmuiobj-tree-container")?.getBoundingClientRect();
169 | if(!rect) return;
170 | const x = e.clientX - rect.left;
171 | const y = e.clientY - rect.top;
172 | setConnector({...connector, x2: x, y2: y});
173 | }}
174 | onMouseUp={(e: any) => {
175 | if(!connector) return;
176 | setConnector(null);
177 | if(!e.target.className.includes("llmuiobj-cell-min")) return;
178 | const id = e.target.getAttribute("id");
179 | const path = getCurrPath(id)
180 |
181 | if(path.includes(connector.id)) return;
182 | linkCells(connector.id, id);
183 | path.push(connector.id);
184 | cells.forEach((cell: CellProps) => {
185 | if(path.includes(cell.id) && !cell.isActive) {
186 | toggleCell(cell.id, 'isActive');
187 | } else if(!path.includes(cell.id) && cell.isActive) {
188 | toggleCell(cell.id, 'isActive');
189 | }
190 | });
191 | }}
192 | >
193 |
194 | {connector && (
195 |
202 | )}
203 | {edges}
204 |
205 |
206 | {rows.map((row, i) => (
207 |
208 | {row}
209 |
210 | ))}
211 |
212 |
213 | )
214 | }
215 |
216 | export default CellTree;
--------------------------------------------------------------------------------
/src/components/Generator/Generator.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 | import Generator from "./Generator";
4 |
5 | const meta: Meta = {
6 | component: Generator,
7 | title: "tsook/Generator",
8 | argTypes: {},
9 | };
10 | export default meta;
11 |
12 | type Story = StoryObj;
13 |
14 | export const Primary: Story = (args) => (
15 |
16 | );
17 | Primary.args = {
18 | id: "Generator-id",
19 | parameters: [
20 | {
21 | id: "model",
22 | name: "Model",
23 | value: "gpt-3.5-turbo",
24 | type: "nominal",
25 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "text-davinci-003"],
26 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "text-davinci-003": "D3"},
27 | defaultValue: "gpt-3.5-turbo"
28 | },
29 | {
30 | id: "temperature",
31 | name: "Temperature",
32 | nickname: "temp",
33 | value: 0.7,
34 | type: "continuous",
35 | allowedValues: [0.0, 2.0],
36 | defaultValue: 1.0
37 | },
38 | {
39 | id: "presence_penalty",
40 | name: "Presence Penalty",
41 | nickname: "presence",
42 | value: 0.0,
43 | type: "continuous",
44 | allowedValues: [0.0, 1.0],
45 | defaultValue: 0.0
46 | },
47 | {
48 | id: "top_k",
49 | name: "Top-K",
50 | nickname: "top",
51 | value: 3,
52 | type: "discrete",
53 | allowedValues: [1, 20],
54 | defaultValue: 0
55 | }
56 | ],
57 | color: "#0088ff",
58 | size: "large",
59 | isGenerating: false
60 | };
61 |
62 | export const Small: Story = (args) => (
63 |
64 | );
65 | Small.args = {
66 | id: "Generator-id",
67 | parameters: [
68 | {
69 | id: "model",
70 | name: "Model",
71 | value: "gpt-3.5-turbo",
72 | type: "nominal",
73 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "text-davinci-003"],
74 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "text-davinci-003": "D3"},
75 | defaultValue: "gpt-3.5-turbo"
76 | },
77 | {
78 | id: "temperature",
79 | name: "Temperature",
80 | nickname: "temp",
81 | value: 0.7,
82 | type: "continuous",
83 | allowedValues: [0.0, 2.0],
84 | defaultValue: 1.0
85 | },
86 | {
87 | id: "presence_penalty",
88 | name: "Presence Penalty",
89 | nickname: "presence",
90 | value: 0.0,
91 | type: "continuous",
92 | allowedValues: [0.0, 1.0],
93 | defaultValue: 0.0
94 | },
95 | {
96 | id: "top_k",
97 | name: "Top-K",
98 | nickname: "top",
99 | value: 3,
100 | type: "discrete",
101 | allowedValues: [1, 20],
102 | defaultValue: 0
103 | }
104 | ],
105 | color: "#0088ff",
106 | size: "small",
107 | isGenerating: false
108 | };
109 |
110 | export const Medium: Story = (args) => (
111 |
112 | );
113 | Medium.args = {
114 | id: "Generator-id",
115 | parameters: [
116 | {
117 | id: "model",
118 | name: "Model",
119 | value: "gpt-3.5-turbo",
120 | type: "nominal",
121 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "text-davinci-003"],
122 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "text-davinci-003": "D3"},
123 | defaultValue: "gpt-3.5-turbo"
124 | },
125 | {
126 | id: "temperature",
127 | name: "Temperature",
128 | nickname: "temp",
129 | value: 0.7,
130 | type: "continuous",
131 | allowedValues: [0.0, 2.0],
132 | defaultValue: 1.0
133 | },
134 | {
135 | id: "presence_penalty",
136 | name: "Presence Penalty",
137 | nickname: "presence",
138 | value: 0.0,
139 | type: "continuous",
140 | allowedValues: [0.0, 1.0],
141 | defaultValue: 0.0
142 | },
143 | {
144 | id: "top_k",
145 | name: "Top-K",
146 | nickname: "top",
147 | value: 3,
148 | type: "discrete",
149 | allowedValues: [1, 20],
150 | defaultValue: 0
151 | }
152 | ],
153 | color: "#0088ff",
154 | size: "medium",
155 | isGenerating: false
156 | };
--------------------------------------------------------------------------------
/src/components/Generator/Generator.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import '@testing-library/jest-dom';
3 | import userEvent from "@testing-library/user-event";
4 | import {render, screen, waitFor } from '@testing-library/react';
5 |
6 | import Generator from "./Generator";
7 |
8 | describe("Generator", () => {
9 | var mockGenerator = {
10 | id: "Generator-id",
11 | parameters: [
12 | {
13 | id: "model",
14 | name: "Model",
15 | value: "gpt-3.5-turbo",
16 | type: "nominal",
17 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "text-davinci-003"],
18 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "text-davinci-003": "D3"},
19 | defaultValue: "gpt-3.5-turbo"
20 | },
21 | {
22 | id: "temperature",
23 | name: "Temperature",
24 | nickname: "temp",
25 | value: 0.7,
26 | type: "continuous",
27 | allowedValues: [0.0, 2.0],
28 | defaultValue: 1.0
29 | },
30 | {
31 | id: "presence_penalty",
32 | name: "Presence Penalty",
33 | nickname: "presence",
34 | value: 0.0,
35 | type: "continuous",
36 | allowedValues: [0.0, 1.0],
37 | defaultValue: 0.0
38 | },
39 | {
40 | id: "top_k",
41 | name: "Top-K",
42 | nickname: "top",
43 | value: 3,
44 | type: "discrete",
45 | allowedValues: [1, 20],
46 | defaultValue: 0
47 | }
48 | ],
49 | color: "#0088ff",
50 | size: "large",
51 | isGenerating: false,
52 | isSelected: false,
53 | cellId: null,
54 | lensId: null
55 | };
56 |
57 | test("renders Generator component", () => {
58 | render( );
59 | expect(screen.getByTestId("Generator-id")).toBeInTheDocument();
60 | expect(screen.getByText("model")).toBeInTheDocument();
61 | expect(screen.getByText("temp")).toBeInTheDocument();
62 | expect(screen.getByText("presence")).toBeInTheDocument();
63 | expect(screen.getByText("top")).toBeInTheDocument();
64 | expect(screen.getByText("3.5")).toBeInTheDocument();
65 | expect(screen.getByText("0.7")).toBeInTheDocument();
66 | expect(screen.getByText("0")).toBeInTheDocument();
67 | expect(screen.getByText("3")).toBeInTheDocument();
68 | });
69 |
70 | test("renders Generator component with isGenerating", () => {
71 | render( );
72 | const svg = screen.getByTestId("Generator-id").querySelector("svg");
73 | expect(svg).toBeInTheDocument();
74 | });
75 |
76 | test("renders parameter control panel", async () => {
77 | render( );
78 | const presence = screen.getByText("presence");
79 | expect(presence).toBeInTheDocument();
80 | const presenceParent = presence.parentElement as HTMLElement;
81 | userEvent.click(presenceParent);
82 | await waitFor(() => {
83 | expect(screen.getByText("Presence Pen")).toBeInTheDocument();
84 | });
85 | });
86 | });
--------------------------------------------------------------------------------
/src/components/Generator/Generator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { GeneratorProps, ParameterProps } from "./Generator.types";
4 | import { ObjectsContext } from "../../context/ObjectsContextProvider";
5 |
6 | const ParameterRow = styled.div<{ size: string }>`
7 | display: flex;
8 | flex-direction: row;
9 | align-items: center;
10 | justify-content: center;
11 | gap: ${(props) => props.size === "small" ? "4px" : (props.size === "medium" ? "6px" : "8px")};
12 | margin-top: ${(props) => props.size === "small" ? "4px" : (props.size === "medium" ? "6px" : "8px")};
13 | `;
14 |
15 | const ParameterBlock = styled.div<{ size: string }>`
16 | display: flex;
17 | flex-direction: column;
18 | align-items: center;
19 | justify-content: center;
20 | background-color: rgba(255, 255, 255, 0.50);
21 | border-radius: ${(props) => props.size === "small" ? "2px" : (props.size == "medium" ? "4px" : "6px")};
22 | height: ${(props) => props.size === "small" ? "32px" : (props.size == "medium" ? "48px" : "64px")};
23 | width: ${(props) => props.size === "small" ? "32px" : (props.size == "medium" ? "48px" : "64px")};
24 | padding: ${(props) => props.size === "small" ? "0 2px 2px 2px" : (props.size == "medium" ? "0 3px 3px 3px" : "0 4px 4px 4px")};
25 | cursor: pointer;
26 | &:hover {
27 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.25);
28 | }
29 | `;
30 |
31 | const ParameterName = styled.div<{ size: string }>`
32 | font-size: ${(props) => props.size === "small" ? "8px" : (props.size === "medium" ? "12px" : "16px")};
33 | color: #555555;
34 | margin-bottom: 2px;
35 | `;
36 |
37 | const ParameterValue = styled.div<{ size: string }>`
38 | font-size: ${(props) => props.size === "small" ? "16px" : (props.size === "medium" ? "20px" : "24px")};
39 | width: 90%;
40 | text-align: center;
41 | color: #333333;
42 | background-color: rgba(255, 255, 255, 0.75);
43 | flex: 1;
44 | border-radius: ${(props) => props.size === "small" ? "2px" : (props.size == "medium" ? "3px" : "4px")};
45 | display: flex;
46 | align-items: center;
47 | justify-content: center;
48 | `;
49 |
50 | const Parameter: React.FC void }> = ({
51 | id,
52 | name,
53 | nickname,
54 | type,
55 | value,
56 | allowedValues,
57 | valueNicknames,
58 | defaultValue,
59 | size,
60 | onClick
61 | }) => {
62 | if(type === "nominal") {
63 | value = valueNicknames ? valueNicknames[value as string] : value;
64 | }
65 |
66 | return (
67 |
68 |
69 | {nickname ? nickname : name.toLowerCase()}
70 |
71 |
72 | {value}
73 |
74 |
75 | )
76 | }
77 |
78 | const ControllerContainer = styled.div<{ size: string, row: number, column: number, color: string }>`
79 | position: absolute;
80 | top: ${(props) => props.size === "small" ? 12 + props.row*(34 + 4) : (props.size === "medium" ? 17 + props.row*(51 + 6) : 22 + props.row*(68 + 8))}px;
81 | left: ${(props) => props.size === "small" ? 4 + props.column*(36 + 4) : (props.size == "medium" ? 6 + props.column*(54 + 6) : 8 + props.column*(72 + 8))}px;
82 | background-color: rgba(255, 255, 255, 0.95);
83 | border-radius: ${(props) => props.size === "small" ? "2px" : (props.size == "medium" ? "4px" : "6px")};
84 | height: ${(props) => props.size === "small" ? "34px" : (props.size == "medium" ? "51px" : "68px")};
85 | width: ${(props) => props.size === "small" ? "120px" : (props.size == "medium" ? "180px" : "220px")};
86 | border: solid ${(props) => props.color} ${(props) => props.size === "small" ? "1px" : "2px"};
87 | display: flex;
88 | align-items: center;
89 | justify-content: center;
90 | box-sizing: border-box;
91 | box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
92 | display: flex;
93 | flex-direction: column;
94 | font-size: ${(props) => props.size === "small" ? "10px" : (props.size === "medium" ? "16px" : "20px")};
95 | text-align: left;
96 | cursor: pointer;
97 | z-index: 3;
98 | `;
99 |
100 | const ControllerInner = styled.div<{size: string, filled?: number}>`
101 | display: flex;
102 | flex-direction: row;
103 | align-items: center;
104 | justify-content: space-between;
105 | width: calc(100% - ${(props) => props.size === "small" ? "8px" : (props.size === "medium" ? "16px" : "24px")});
106 | padding: ${(props) => props.size === "small" ? "0 4px" : (props.size == "medium" ? "0 8px" : "0 12px")};
107 |
108 | & > input[type=text] {
109 | font-size: ${(props) => props.size === "small" ? "10px" : (props.size == "medium" ? "14px" : "18px")};
110 | width: 20%;
111 | text-align: center;
112 | border: solid 1px #cccccc;
113 | border-radius: 4px;
114 | }
115 |
116 | & > input[type=range] {
117 | width: 100%;
118 | -webkit-appearance: none;
119 | padding-top: ${(props) => props.size === "small" ? "2px" : (props.size === "medium" ? "4px" : "6px")};
120 | }
121 | & > input[type=range]::-webkit-slider-thumb {
122 | -webkit-appearance: none;
123 | appearance: none;
124 | width: ${(props) => props.size === "small" ? "8px" : (props.size === "medium" ? "12px" : "16px")};
125 | height: ${(props) => props.size === "small" ? "8px" : (props.size === "medium" ? "12px" : "16px")};
126 | border-radius: 50%;
127 | background-color: #0066ffdd;
128 | cursor: pointer;
129 | transition: 0.5s;
130 | margin-top: ${(props) => props.size === "small" ? "-2px" : (props.size === "medium" ? "-3px" : "-4px")};
131 | &:hover {
132 | background-color: #0066ff;
133 | }
134 | }
135 | & > input[type=range]::-webkit-slider-runnable-track {
136 | -webkit-appearance: none;
137 | height: ${(props) => props.size === "small" ? "4px" : (props.size === "medium" ? "6px" : "8px")};
138 | border-radius: 20px;
139 | width: 100%;
140 | background: #dddddd;
141 | background-image: linear-gradient(#0066ffaa, #0066ffaa);
142 | background-size: ${(props) => props.filled}% 100%;
143 | background-repeat: no-repeat;
144 | }
145 | & > select {
146 | width: 100%;
147 | font-size: ${(props) => props.size === "small" ? "10px" : (props.size == "medium" ? "14px" : "18px")};
148 | border: solid 1px #cccccc;
149 | border-radius: 4px;
150 | }
151 | `;
152 |
153 | const ParameterController: React.FC<{ parameter: ParameterProps, size: string, row: number, column: number, color: string, changeHandler: (value: string | number) => void }> = ({
154 | parameter,
155 | size,
156 | row,
157 | column,
158 | color,
159 | changeHandler
160 | }) => {
161 | const [currInput, setCurrInput] = React.useState(parameter.value !== undefined && typeof parameter.value !== "string" ? parameter.value.toString() : "");
162 |
163 | React.useEffect(() => {
164 | setCurrInput(parameter.value !== undefined && typeof parameter.value !== "string" ? parameter.value.toString() : "");
165 | }, [parameter.value]);
166 |
167 | const handleChange = () => {
168 | var inputFloat = parseFloat(currInput);
169 | if(isNaN(inputFloat)) {
170 | setCurrInput(parameter.value && typeof parameter.value !== "string" ? parameter.value.toString() : "");
171 | return;
172 | }
173 |
174 | var allowedValues = parameter.allowedValues as number[];
175 | if(inputFloat > allowedValues[1]) {
176 | inputFloat = allowedValues[1];
177 | } else if(inputFloat < allowedValues[0]) {
178 | inputFloat = allowedValues[0];
179 | }
180 |
181 | if(parameter.type === "discrete") {
182 | inputFloat = Math.round(inputFloat);
183 | }
184 |
185 | changeHandler(inputFloat);
186 | }
187 |
188 | var filled = 0;
189 | if(parameter.type !== "nominal") {
190 | filled = parameter.value as number / ((parameter.allowedValues[1] as number) - (parameter.allowedValues[0] as number)) * 100;
191 | }
192 |
193 | return (
194 |
195 | e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
196 | {parameter.name.slice(0, 12)}
197 | {parameter.type !== "nominal" ?
198 | ) => setCurrInput(e.target.value)}
201 | onKeyDown={(e: React.KeyboardEvent) => e.key == "Enter" && handleChange()}
202 | onBlur={handleChange}
203 | type="text"
204 | /> :
205 | ""
206 | }
207 |
208 | e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
209 | {parameter.type !== "nominal" ?
210 | changeHandler(parseFloat(e.target.value))}
217 | /> :
218 | changeHandler(e.target.value)}>
219 | {parameter.allowedValues.map((value, index) => {
220 | return (
221 | {value}
222 | )
223 | })}
224 |
225 | }
226 |
227 |
228 | )
229 | };
230 |
231 | const GeneratorContainer = styled.div<{ color: string, size: string }>`
232 | display: flex;
233 | flex-direction: column;
234 | align-items: center;
235 | justify-content: center;
236 | background-color: ${(props) => props.color};
237 | border-radius: ${(props) => props.size === "small" ? "4px" : (props.size == "medium" ? "6px" : "8px")};
238 | padding: ${(props) => props.size === "small" ? "2px 4px 4px 4px" : (props.size == "medium" ? "3px 6px 6px 6px" : "4px 8px 8px 8px")};
239 | box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
240 | width: fit-content;
241 | height: fit-content;
242 | position: relative;
243 | cursor: pointer;
244 | `;
245 |
246 | const GeneratorHeader = styled.div<{ size: string }>`
247 | font-size: ${(props) => props.size === "small" ? "8px" : (props.size === "medium" ? "12px" : "16px")};
248 | line-height: 1;
249 | color: white;
250 | user-select: none;
251 | `;
252 |
253 | const AnimationContainer = styled.svg<{ size: string }>`
254 | position: absolute;
255 | top: calc(50% - ${(props) => props.size === "small" ? "16px" : (props.size == "medium" ? "24px" : "32px")});
256 | left: calc(50% - ${(props) => props.size === "small" ? "16px" : (props.size == "medium" ? "24px" : "32px")});
257 | height: ${(props) => props.size === "small" ? "32px" : (props.size == "medium" ? "48px" : "64px")};
258 | width: ${(props) => props.size === "small" ? "32px" : (props.size == "medium" ? "48px" : "64px")};
259 | z-index: 2;
260 | `;
261 |
262 | const Outline = styled.div<{ size: string }>`
263 | position: absolute;
264 | top: -${(props) => props.size !== "large" ? 4 : 6}px;
265 | left: -${(props) => props.size !== "large" ? 4 : 6}px;
266 | height: calc(100% + ${(props) => props.size !== "large" ? 8 : 12}px);
267 | width: calc(100% + ${(props) => props.size !== "large" ? 8 : 12}px);
268 | border: solid rgb(0, 194, 255) ${(props) => props.size !== "large" ? 2 : 3}px;
269 | border-radius: ${(props) => props.size === "small" ? 6 : (props.size == "medium" ? 8 : 10)}px;
270 | box-sizing: border-box;
271 | box-shadow: 0 0 2px 2px rgba(0, 194, 255, 0.3);
272 | `;
273 |
274 | const Generator: React.FC = ({
275 | id,
276 | parameters,
277 | color,
278 | size,
279 | numColumns,
280 | isGenerating,
281 | isSelected,
282 | onMouseEnter,
283 | onMouseLeave
284 | }) => {
285 | const { updateGenerator, toggleGenerator, onGenerate } = React.useContext(ObjectsContext);
286 | const [selectedParameter, setSelectedParameter] = React.useState("");
287 | const clickTimer = React.useRef(null);
288 |
289 | React.useEffect(() => {
290 | const handleClickOutside = (event: any) => {
291 | if (event.target.id !== id) setSelectedParameter("");
292 | }
293 | document.addEventListener("mousedown", handleClickOutside);
294 |
295 | return () => {
296 | document.removeEventListener("mousedown", handleClickOutside);
297 | }
298 | }, []);
299 |
300 | const handleClick = (e: any) => {
301 | if(e.detail === 1) {
302 | clickTimer.current = window.setTimeout(() => {
303 | onGenerate(id);
304 | clickTimer.current = null;
305 | }, 200);
306 | } else if(e.detail === 2) {
307 | if(clickTimer.current) {
308 | clearTimeout(clickTimer.current);
309 | clickTimer.current = null;
310 | }
311 | toggleGenerator(id, 'isSelected');
312 | }
313 | }
314 |
315 | const handleMouseEnter = (e: any) => {
316 | if(onMouseEnter) onMouseEnter(e);
317 | }
318 | const handleMouseLeave = (e: any) => {
319 | if(onMouseLeave) onMouseLeave(e);
320 | }
321 |
322 | const parameterRows: ParameterProps[][] = [];
323 | let currentRow: ParameterProps[] = [];
324 | parameters.forEach((parameter, index) => {
325 | if (index % (numColumns ? numColumns : 2) === 0 && index !== 0) {
326 | parameterRows.push(currentRow);
327 | currentRow = [];
328 | }
329 | currentRow.push(parameter);
330 | });
331 | parameterRows.push(currentRow);
332 |
333 | const selectedParameterIdx = parameters.findIndex((parameter) => parameter.id === selectedParameter);
334 |
335 | return (
336 |
345 | Generate
346 | {parameterRows.map((row, index) => {
347 | return (
348 |
353 | {row.map((parameter, index) => {
354 | return (
355 | {
360 | e.stopPropagation();
361 | setSelectedParameter(parameter.id);
362 | }}
363 | />
364 | )
365 | })}
366 |
367 | )
368 | })}
369 | {selectedParameterIdx != -1 && (
370 | {
377 | const newParameters = [...parameters];
378 | newParameters[selectedParameterIdx].value = value;
379 | updateGenerator(id, newParameters);
380 | }}
381 | />
382 | )}
383 | {isGenerating && (
384 |
385 |
386 |
389 |
390 |
391 |
392 |
393 | )}
394 | {isSelected && (
395 |
396 | )}
397 |
398 | )
399 | }
400 |
401 | export default Generator;
--------------------------------------------------------------------------------
/src/components/Generator/Generator.types.ts:
--------------------------------------------------------------------------------
1 | export interface GeneratorProps {
2 | id: string;
3 | parameters: ParameterProps[];
4 | color: string;
5 | size?: string;
6 | numColumns?: number;
7 | isGenerating?: boolean;
8 | isSelected?: boolean;
9 | cellId: string | null;
10 | lensId: string | null;
11 | onMouseEnter?: (e: React.MouseEvent) => void;
12 | onMouseLeave?: (e: React.MouseEvent) => void;
13 | }
14 |
15 | export interface ParameterProps {
16 | id: string;
17 | name: string;
18 | nickname?: string;
19 | type: string;
20 | allowedValues: string[] | number[];
21 | valueNicknames?: {[key: string]: string};
22 | defaultValue: string | number;
23 | value?: string | number;
24 | }
--------------------------------------------------------------------------------
/src/components/Lens/Lens.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 | import Lens from "./Lens";
4 |
5 | const meta: Meta = {
6 | component: Lens,
7 | title: "tsook/Lens",
8 | argTypes: {},
9 | };
10 | export default meta;
11 |
12 | type Story = StoryObj;
13 |
14 | export const Primary: Story = (args) => (
15 |
16 | );
17 | Primary.args = {
18 | id: "Lens-id",
19 | type: "list",
20 | style: {
21 | "width": "600px",
22 | "height": "400px"
23 | },
24 | onGenerationClick: (text: string) => console.log(text)
25 | };
--------------------------------------------------------------------------------
/src/components/Lens/Lens.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import '@testing-library/jest-dom';
3 | import {render, screen, waitFor } from '@testing-library/react';
4 |
5 | import Lens from "./Lens";
6 |
7 | describe("Lens", () => {
8 | var mockLens = {
9 | id: "Lens-id",
10 | type: "list",
11 | group: 0
12 | };
13 |
14 | test("renders List Lens component", () => {
15 | render( );
16 | expect(screen.getByTestId("Lens-id")).toBeInTheDocument();
17 | });
18 |
19 | // TODO: test case for clicking on a information button
20 | });
--------------------------------------------------------------------------------
/src/components/Lens/Lens.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { LensProps, GenerationProps } from "./Lens.types";
4 |
5 | import { ObjectsContext } from "../../context/ObjectsContextProvider";
6 |
7 | import ListLens from "./ListLens";
8 | import SpaceLens from "./SpaceLens";
9 | import PlotLens from "./PlotLens";
10 | import RatingLens from "./RatingLens";
11 |
12 | const LensContainer = styled.div`
13 | flex: 1;
14 | `;
15 |
16 | const Lens: React.FC = ({
17 | id,
18 | type,
19 | style,
20 | onGenerationClick,
21 | getGenerationMetadata
22 | }) => {
23 | const { generations, lenses } = React.useContext(ObjectsContext);
24 |
25 | const currLens = lenses.find((lens) => lens.id === id);
26 |
27 | const groupedLensIds = lenses.map((lens) => currLens?.group === lens.group ? lens.id : null);
28 | const generationsData = generations.filter((generation) => {
29 | if(generation.lensId === null) return false;
30 | if(generation.lensId === id) return true;
31 | if(groupedLensIds.includes(generation.lensId)) return true;
32 | return false;
33 | });
34 |
35 | return (
36 |
41 | {type === "list" && (
42 |
46 | )}
47 | {type === "space" && getGenerationMetadata && (
48 |
53 | )}
54 | {type === "plot" && getGenerationMetadata && (
55 |
60 | )}
61 | {type === "rating" && getGenerationMetadata && (
62 |
67 | )}
68 |
69 | )
70 | }
71 |
72 | export default Lens;
--------------------------------------------------------------------------------
/src/components/Lens/Lens.types.ts:
--------------------------------------------------------------------------------
1 | import { ParameterProps } from "../Generator/Generator.types";
2 |
3 | export interface LensProps {
4 | id: string;
5 | type: string;
6 | style?: React.CSSProperties;
7 | onGenerationClick?: (generationText: string) => void;
8 | group: number;
9 | getGenerationMetadata?: (generations: GenerationProps[]) => Promise;
10 | }
11 |
12 | export interface GenerationProps {
13 | id: string;
14 | generatorId: string;
15 | lensId: string | null;
16 | inputText: string;
17 | content: string;
18 | parameters: ParameterProps[];
19 | metadata: any;
20 | }
--------------------------------------------------------------------------------
/src/components/Lens/ListLens.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { GenerationProps } from "./Lens.types";
4 | import { ParameterProps } from "../Generator/Generator.types";
5 |
6 | import { ObjectsContext } from "../../context/ObjectsContextProvider";
7 |
8 | const InputIcon = (
9 |
10 |
11 |
12 | );
13 |
14 | const ParameterIcon = (
15 |
16 |
17 |
18 | );
19 |
20 | const ListLensContainer = styled.div`
21 | display: flex;
22 | flex-direction: column;
23 | gap: 8px;
24 | padding-right: 4px;
25 | max-height: 100%;
26 |
27 | overflow-y: auto;
28 |
29 | &::-webkit-scrollbar {
30 | width: 4px;
31 | }
32 | &::-webkit-scrollbar-track {
33 | background: #f1f1f1;
34 | border-radius: 4px;
35 | }
36 | &::-webkit-scrollbar-thumb {
37 | background: #ddd;
38 | border-radius: 4px;
39 | }
40 | `;
41 |
42 | const InfoContainer = styled.div`
43 | display: flex;
44 | flex-direction: row;
45 | gap: 4px;
46 | `;
47 |
48 | const InfoButton = styled.div<{ opened: number }>`
49 | min-width: 24px;
50 | height: ${props => props.opened ? "inherit" : "24px"};
51 | border-radius: 4px;
52 | border: solid 2px ${props => props.opened ? "#0088ff" : "#cccccc"};
53 | display: flex;
54 | justify-content: center;
55 | align-items: center;
56 | cursor: pointer;
57 | fill: ${props => props.opened ? "#0088ff" : "#cccccc"};
58 |
59 | &:hover {
60 | border-color: #0088ffcc;
61 | fill: #0088ffcc;
62 | }
63 | `;
64 |
65 | const InfoContent = styled.div`
66 | display: flex;
67 | flex-direction: column;
68 | gap: 4px;
69 | flex-shrink: 1;
70 | justify-content: center;
71 | border: solid 2px #0088ff66;
72 | border-radius: 4px;
73 | padding: 4px 8px;
74 | color: #555555;
75 | font-size: 14px;
76 | max-height: 200px;
77 | overflow-y: auto;
78 | word-break: break-word;
79 | white-space: pre-wrap;
80 |
81 | &::-webkit-scrollbar {
82 | width: 4px;
83 | }
84 | &::-webkit-scrollbar-track {
85 | background: #f1f1f1;
86 | border-radius: 4px;
87 | }
88 | &::-webkit-scrollbar-thumb {
89 | background: #ddd;
90 | border-radius: 4px;
91 | }
92 | `;
93 |
94 | const Section = styled.div`
95 | display: flex;
96 | flex-direction: row;
97 | flex: 1;
98 | gap: 8px;
99 | `;
100 |
101 | const SectionTab = styled.div<{ minimized: number }>`
102 | width: ${props => props.minimized ? "28px" : "4px"};
103 | height: ${props => props.minimized ? "4px" : "inherit"};
104 | background-color: #dddddd;
105 | border-radius: 4px;
106 | cursor: pointer;
107 | &:hover {
108 | background-color: #0088ffcc;
109 | }
110 | `;
111 |
112 | const SectionContent = styled.div`
113 | display: flex;
114 | flex: 1;
115 | flex-direction: column;
116 | gap: 8px;
117 | `;
118 |
119 |
120 | const OutputContainer = styled.div<{ viewed: number, hovered: number }>`
121 | border: solid 2px #cccccc;
122 | border-radius: 8px;
123 | padding: 4px 8px;
124 | color: #555555;
125 | cursor: pointer;
126 | font-size: 14px;
127 | font-weight: ${props => !props.viewed ? "bold" : "normal"};
128 |
129 | ${({hovered}) => hovered && `
130 | border-color: #0088ff;
131 | background-color: #0088ff22;
132 | `}
133 | `;
134 |
135 | const parametersToStr = (properties: ParameterProps[]) => {
136 | var str = "";
137 | for(var i = 0; i < properties.length; i++) {
138 | var prop = properties[i];
139 | str += prop.name + "=" + prop.value;
140 | if(i < properties.length - 1) {
141 | str += ";";
142 | }
143 | }
144 | return str;
145 | }
146 |
147 | const sectionGenerations = (generations: GenerationProps[]) => {
148 | const sections: {[key: string]: {[key: string]: {id: string, content: string}[]}} = {};
149 | for(var i = 0; i < generations.length; i++) {
150 | const generation = generations[i];
151 | const inputText = generation.inputText;
152 | const parametersStr = parametersToStr(generation.parameters);
153 | if(!(inputText in sections)) {
154 | sections[inputText] = {};
155 | }
156 | if(!(parametersStr in sections[inputText])) {
157 | sections[inputText][parametersStr] = [];
158 | }
159 | sections[inputText][parametersStr].push({
160 | id: generation.id,
161 | content: generation.content
162 | });
163 | }
164 | return sections;
165 | }
166 |
167 | interface ListLensProps {
168 | generations: GenerationProps[];
169 | onGenerationClick?: (generationText: string) => void;
170 | }
171 |
172 | const ListLens: React.FC = ({
173 | generations,
174 | onGenerationClick
175 | }) => {
176 | const { hoveredId, setHoveredId } = React.useContext(ObjectsContext);
177 |
178 | const [viewed, setViewed] = React.useState([]);
179 | const [minimized, setMinimized] = React.useState([]);
180 | const [openInfo, setOpenInfo] = React.useState([]);
181 |
182 | const sections = sectionGenerations(generations);
183 |
184 | return (
185 |
186 | {generations.length === 0 && (
187 |
188 | No generations yet...
189 |
190 | )}
191 | {Object.keys(sections).map((inputText, i) => ([
192 |
193 | {
196 | const sectionId = `input-${i}`;
197 | if(openInfo.includes(sectionId)) {
198 | setOpenInfo(openInfo.filter(id => id !== sectionId));
199 | } else {
200 | setOpenInfo([...openInfo, sectionId]);
201 | }
202 | }}
203 | >
204 | {InputIcon}
205 |
206 | {openInfo.includes(`input-${i}`) && (
207 |
208 | {inputText}
209 |
210 | )}
211 | ,
212 |
213 | {
216 | const sectionId = `input-${i}`;
217 | if(minimized.includes(sectionId)) {
218 | setMinimized(minimized.filter(id => id !== sectionId));
219 | } else {
220 | setMinimized([...minimized, sectionId]);
221 | }
222 | }}
223 | />
224 | {!minimized.includes(`input-${i}`) && (
225 |
226 | {Object.keys(sections[inputText]).map((parametersStr, j) => ([
227 |
228 | {
231 | const sectionId = `parameters-${i}-${j}`;
232 | if(openInfo.includes(sectionId)) {
233 | setOpenInfo(openInfo.filter(id => id !== sectionId));
234 | } else {
235 | setOpenInfo([...openInfo, sectionId]);
236 | }
237 | }}
238 | >
239 | {ParameterIcon}
240 |
241 | {openInfo.includes(`parameters-${i}-${j}`) && (
242 |
243 | {parametersStr.split(";").map((parameter, k) => {
244 | const [name, value] = parameter.split("=");
245 | return (
246 |
247 | {name}: {value}
248 |
249 | )
250 | })}
251 |
252 | )}
253 | ,
254 |
255 | {
258 | const sectionId = `parameters-${i}-${j}`;
259 | if(minimized.includes(sectionId)) {
260 | setMinimized(minimized.filter(id => id !== sectionId));
261 | } else {
262 | setMinimized([...minimized, sectionId]);
263 | }
264 | }}
265 | />
266 | {!minimized.includes(`parameters-${i}-${j}`) && (
267 |
268 | {sections[inputText][parametersStr].map((generation, k) => (
269 | {
274 | if(!viewed.includes(generation.id)) {
275 | setViewed([...viewed, generation.id]);
276 | }
277 | setHoveredId(generation.id);
278 | }}
279 | onMouseOut={() => {
280 | setHoveredId(null);
281 | }}
282 | onClick={() => {
283 | if(!onGenerationClick) return;
284 | onGenerationClick(generation.content);
285 | }}
286 | >
287 | {generation.content}
288 |
289 | ))}
290 |
291 | )}
292 |
293 | ]))}
294 |
295 | )}
296 |
297 | ]))}
298 |
299 | )
300 | }
301 |
302 | export default ListLens;
--------------------------------------------------------------------------------
/src/components/Lens/PlotLens.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { GenerationProps } from "./Lens.types";
4 | import { ParameterProps } from "../Generator/Generator.types";
5 |
6 | import { ObjectsContext } from "../../context/ObjectsContextProvider";
7 |
8 | const PlotLensContainer = styled.div`
9 | display: flex;
10 | flex-direction: row;
11 | gap: 8px;
12 | padding-right: 4px;
13 | width: 100%;
14 | height: 100%;
15 | `;
16 |
17 | const PlotContainer = styled.div`
18 | flex: 1;
19 | display: flex;
20 | flex-direction: column;
21 | gap: 8px;
22 | `;
23 |
24 | const Plot = styled.div`
25 | flex: 1;
26 | position: relative;
27 | border-left: solid 1px #ccc;
28 | border-bottom: solid 1px #ccc;
29 | `;
30 |
31 | const Dot = styled.div<{x: number, y: number, selected: boolean, viewed: boolean}>`
32 | position: absolute;
33 | top: ${({y}) => y}%;
34 | left: ${({x}) => x}%;
35 | width: 12px;
36 | height: 12px;
37 | border-radius: 50%;
38 | background-color: ${({viewed}) => viewed ? "#aaaaaa99" : "#0088ff99"};
39 | cursor: pointer;
40 | ${({selected}) => selected && `
41 | width: 16px;
42 | height: 16px;
43 | margin-left: -4px;
44 | margin-top: -4px;
45 | background-color: #0088ff;
46 | box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.2);
47 | `}
48 | &:hover {
49 | width: 16px;
50 | height: 16px;
51 | margin-left: -4px;
52 | margin-top: -4px;
53 | background-color: #0088ff;
54 | box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.2);
55 | }
56 | `;
57 |
58 | const InfoContainer = styled.div`
59 | display: flex;
60 | flex-direction: column;
61 | gap: 4px;
62 | flex: 1;
63 | height: 100%;
64 | width: 0;
65 | `;
66 |
67 | const InfoSection = styled.div`
68 | display: flex;
69 | padding: 8px;
70 | border-radius: 8px;
71 | font-size: 10px;
72 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.2);
73 | gap: 4px;
74 | flex-direction: column;
75 | overflow-y: auto;
76 | `;
77 |
78 | const InfoContent = styled.div`
79 | font-size: 12px;
80 | overflow-y: auto;
81 | flex: 1;
82 |
83 | &::-webkit-scrollbar {
84 | width: 4px;
85 | }
86 | &::-webkit-scrollbar-track {
87 | background: #f1f1f1;
88 | border-radius: 4px;
89 | }
90 | &::-webkit-scrollbar-thumb {
91 | background: #ddd;
92 | border-radius: 4px;
93 | }
94 | `;
95 |
96 | const YAxis = styled.div`
97 | height: 100%;
98 | color: #aaa;
99 | display: flex;
100 | justify-content: center;
101 | align-items: center;
102 | font-size: 12px;
103 | & > span {
104 | transform: rotate(180deg);
105 | writing-mode: tb-rl;
106 | }
107 | `;
108 |
109 | const XAxis = styled.div`
110 | width: 100%;
111 | color: #aaa;
112 | display: flex;
113 | justify-content: center;
114 | align-items: center;
115 | font-size: 12px;
116 | `;
117 |
118 | const SelectorContainer = styled.div`
119 | display: flex;
120 | gap: 4px;
121 | font-size: 12px;
122 | color: #999;
123 | width: 100%;
124 |
125 | & > select {
126 | flex: 1;
127 | font-size: 12px;
128 | border: solid 1px #cccccc;
129 | border-radius: 4px;
130 | box-sizing: border-box;
131 | }
132 | `;
133 |
134 | const AnimationContainer = styled.svg`
135 | position: absolute;
136 | top: calc(50% - 24px);
137 | left: calc(50% - 24px);
138 | height: 48px;
139 | width: 48px;
140 | z-index: 2;
141 | `;
142 |
143 | const parametersToHtml = (properties: ParameterProps[]) => {
144 | return properties.map((property: ParameterProps) => {
145 | const { name, value } = property;
146 | return (
147 |
148 | {name}: {value}
149 |
150 | )
151 | })
152 | }
153 |
154 | interface PlotLensProps {
155 | generations: GenerationProps[];
156 | onGenerationClick?: (generationText: string) => void;
157 | getRatings: (generations: GenerationProps[]) => Promise<{[rating: string]: number}[]>;
158 | }
159 |
160 | const PlotLens: React.FC = ({
161 | generations,
162 | onGenerationClick,
163 | getRatings
164 | }) => {
165 | const { hoveredId, setHoveredId, updateGenerationsData } = React.useContext(ObjectsContext);
166 |
167 | const [loadingCount, setLoadingCount] = React.useState(0);
168 |
169 | const [selectedId, setSelectedId] = React.useState(null);
170 | const [viewed, setViewed] = React.useState([]);
171 |
172 | const [allDimensions, setAllDimensions] = React.useState([]);
173 | const [dimensions, setDimensions] = React.useState<{x: string | null, y: string | null}>({x: null, y: null});
174 |
175 | const prevGenerations = React.useRef([]);
176 |
177 | React.useEffect(() => {
178 | if(generations.length === prevGenerations.current.length) return;
179 |
180 | const newGenerations = generations.filter((generation: GenerationProps) => {
181 | return !prevGenerations.current.find((prevGeneration: GenerationProps) => prevGeneration.id === generation.id);
182 | });
183 |
184 | if(newGenerations.length === 0) return;
185 |
186 | const unprocessedGenerations = newGenerations.filter((generation: GenerationProps) => {
187 | const { metadata } = generation;
188 | if(!metadata) return true;
189 | const { ratings } = metadata;
190 | return !ratings;
191 | });
192 |
193 | if(unprocessedGenerations.length === 0 && (dimensions.x == null || dimensions.y == null)) {
194 | const ratings = newGenerations[0].metadata.ratings;
195 | const ratingDimensions = Object.keys(ratings);
196 | setAllDimensions(ratingDimensions);
197 | setDimensions({x: ratingDimensions[0], y: ratingDimensions[1]});
198 | return;
199 | }
200 |
201 | setLoadingCount((prev) => prev + 1);
202 | getRatings(unprocessedGenerations).then((ratings: {[key: string]: number}[]) => {
203 | if(ratings.length === 0) {
204 | setLoadingCount((prev) => prev - 1);
205 | return;
206 | }
207 |
208 | const ratingDimensions = Object.keys(ratings[0]);
209 |
210 | updateGenerationsData(
211 | unprocessedGenerations.map((generation) => generation.id),
212 | ratings.map((ratings) => ({ratings}))
213 | );
214 |
215 | if(dimensions.x == null || dimensions.y == null) {
216 | setAllDimensions(ratingDimensions);
217 | setDimensions({x: ratingDimensions[0], y: ratingDimensions[1]});
218 | }
219 |
220 | setLoadingCount((prev) => prev - 1);
221 | });
222 |
223 | prevGenerations.current = generations;
224 | }, [generations]);
225 |
226 | const hoveredGeneration = generations.find((generation: GenerationProps) => generation.id === hoveredId);
227 | const selectedGeneration = generations.find((generation: GenerationProps) => generation.id === selectedId);
228 |
229 | return (
230 |
231 |
232 |
233 | X
234 | setDimensions({...dimensions, x: e.target.value})}>
235 | {allDimensions.map((dimension) => (
236 | {dimension}
237 | ))}
238 |
239 | Y
240 | setDimensions({...dimensions, y: e.target.value})}>
241 | {allDimensions.map((dimension) => (
242 | {dimension}
243 | ))}
244 |
245 |
246 |
247 | {dimensions.y ? dimensions.y : "y-axis"}
248 | {loadingCount > 0 && (
249 |
250 |
251 |
254 |
255 |
256 |
257 |
258 | )}
259 |
260 | {generations.length == 0 && (
261 |
262 | No generations yet...
263 |
264 | )}
265 | {generations.map((generation: GenerationProps, index: number) => {
266 | const { metadata } = generation;
267 | if(!metadata) return null;
268 | const { ratings } = metadata;
269 | if(!ratings) return null;
270 |
271 | // normalize x and y
272 | const noise = (index % 15) / 5;
273 | const normalizedX = (dimensions.x == null) ? 8 : ratings[dimensions.x] * 84 + 8 + noise;
274 | const normalizedY = (dimensions.y == null) ? 8 : ratings[dimensions.y] * 84 + 8 + noise;
275 |
276 | return (
277 | setSelectedId(generation.id)}
281 | onMouseOver={() => {
282 | setHoveredId(generation.id);
283 | setViewed([...viewed, generation.id]);
284 | }}
285 | onMouseOut={() => setHoveredId(null)}
286 | selected={selectedId === generation.id}
287 | viewed={viewed.includes(generation.id)}
288 | />
289 | )
290 | })}
291 |
292 |
293 | {dimensions.x ? dimensions.x : "x-axis"}
294 |
295 |
296 |
297 | Input
298 |
299 | {hoveredGeneration ?
300 | hoveredGeneration.inputText :
301 | (selectedGeneration ? selectedGeneration.inputText : "Hover over a generation")
302 | }
303 |
304 |
305 |
306 | Parameters
307 |
308 | {hoveredGeneration ?
309 | parametersToHtml(hoveredGeneration.parameters) :
310 | (selectedGeneration ? parametersToHtml(selectedGeneration.parameters) : "Hover over a generation")
311 | }
312 |
313 |
314 | selectedGeneration && onGenerationClick && onGenerationClick(selectedGeneration.content)}
317 | >
318 | Output
319 |
320 | {hoveredGeneration ?
321 | hoveredGeneration.content :
322 | (selectedGeneration ? selectedGeneration.content : "Hover over a generation")
323 | }
324 |
325 |
326 |
327 |
328 | )
329 | }
330 |
331 | export default PlotLens;
--------------------------------------------------------------------------------
/src/components/Lens/RatingLens.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { GenerationProps } from "./Lens.types";
4 | import { ParameterProps } from "../Generator/Generator.types";
5 |
6 | import { ObjectsContext } from "../../context/ObjectsContextProvider";
7 |
8 | const RatingLensContainer = styled.div`
9 | display: flex;
10 | flex-direction: column;
11 | gap: 24px;
12 | width: 100%;
13 | height: 100%;
14 | overflow-y: auto;
15 | box-sizing: border-box;
16 | position: relative;
17 | padding-right: 4px;
18 |
19 | &::-webkit-scrollbar {
20 | width: 4px;
21 | }
22 | &::-webkit-scrollbar-track {
23 | background: #f1f1f1;
24 | border-radius: 4px;
25 | }
26 | &::-webkit-scrollbar-thumb {
27 | background: #ddd;
28 | border-radius: 4px;
29 | }
30 | `;
31 |
32 | const GenerationContainer = styled.div<{selected: boolean, hovered: boolean}>`
33 | display: flex;
34 | flex-direction: column;
35 | gap: 8px;
36 | padding: 8px;
37 | border-radius: 8px;
38 | background-color: ${(props) => props.selected ? "#0088ff44" : (props.hovered ? "#0088ff22" : "#fff")};
39 | cursor: pointer;
40 | border: solid 1px ${(props) => props.selected ? "#0088ff" : (props.hovered ? "#0088ff66" : "#ccc")};
41 | `;
42 |
43 | const GenerationText = styled.div`
44 | font-size: 12px;
45 | color: #333;
46 | border-radius: 4px;
47 | padding: 8px;
48 | background-color: #fff;
49 | `;
50 |
51 | const RatingContainer = styled.div`
52 | display: flex;
53 | flex-direction: column;
54 | gap: 2px;
55 | position: relative;
56 | `;
57 |
58 | const RatingLabel = styled.div`
59 | font-size: 12px;
60 | color: #fff;
61 | position: absolute;
62 | top: 2px;
63 | left: 8px;
64 | `;
65 |
66 | const ScoreLabel = styled.div`
67 | font-size: 12px;
68 | color: #fff;
69 | position: absolute;
70 | top: 2px;
71 | right: 8px;
72 | `;
73 |
74 | const Bar = styled.div`
75 | width: 100%;
76 | height: 20px;
77 | border-radius: 4px;
78 | background-color: #ccc;
79 | overflow: hidden;
80 | display: flex;
81 | flex-direction: row;
82 | `;
83 |
84 | const BarFilled = styled.div`
85 | height: 100%;
86 | background-color: #0088ff;
87 | `;
88 |
89 | const BarEmpty = styled.div`
90 | height: 100%;
91 | background-color: #aaa;
92 | flex: 1;
93 | `;
94 |
95 | const AnimationContainer = styled.svg`
96 | position: absolute;
97 | top: calc(50% - 24px);
98 | left: calc(50% - 24px);
99 | height: 48px;
100 | width: 48px;
101 | z-index: 2;
102 | `;
103 |
104 | interface RatingLensProps {
105 | generations: GenerationProps[];
106 | onGenerationClick?: (generationText: string) => void;
107 | getRatings: (generations: GenerationProps[]) => Promise<{[rating: string]: number}[]>;
108 | }
109 |
110 | const RatingLens: React.FC = ({
111 | generations,
112 | onGenerationClick,
113 | getRatings
114 | }) => {
115 | const { hoveredId, setHoveredId, updateGenerationsData } = React.useContext(ObjectsContext);
116 |
117 | const [loadingCount, setLoadingCount] = React.useState(0);
118 |
119 | const [selectedId, setSelectedId] = React.useState(null);
120 |
121 | const prevGenerations = React.useRef([]);
122 |
123 | React.useEffect(() => {
124 | if(generations.length === prevGenerations.current.length) return;
125 |
126 | const newGenerations = generations.filter((generation: GenerationProps) => {
127 | return !prevGenerations.current.find((prevGeneration: GenerationProps) => prevGeneration.id === generation.id);
128 | });
129 |
130 | if(newGenerations.length === 0) return;
131 |
132 | const unprocessedGenerations = newGenerations.filter((generation: GenerationProps) => {
133 | const { metadata } = generation;
134 | if(!metadata) return true;
135 | const { ratings } = metadata;
136 | return !ratings;
137 | });
138 |
139 | if(unprocessedGenerations.length === 0) return;
140 |
141 | setLoadingCount((prev) => prev + 1);
142 | getRatings(unprocessedGenerations).then((ratings: {[key: string]: number}[]) => {
143 | if(ratings.length === 0) {
144 | setLoadingCount((prev) => prev - 1);
145 | return;
146 | }
147 | updateGenerationsData(
148 | unprocessedGenerations.map((generation) => generation.id),
149 | ratings.map((ratings) => ({ratings}))
150 | );
151 | setLoadingCount((prev) => prev - 1);
152 | });
153 |
154 | prevGenerations.current = generations;
155 | }, [generations]);
156 |
157 | return (
158 |
159 | {loadingCount > 0 && (
160 |
161 |
162 |
165 |
166 |
167 |
168 |
169 | )}
170 | {generations.map((generation) => {
171 | const ratings = generation.metadata?.ratings;
172 |
173 | if(!ratings) return null;
174 |
175 | return (
176 | onGenerationClick ? onGenerationClick(generation.content) : null}
180 | onMouseEnter={() => setHoveredId(generation.id)}
181 | onMouseLeave={() => setHoveredId(null)}
182 | >
183 | {Object.keys(ratings).map((ratingName: string) => {
184 | return (
185 |
186 | {ratingName}
187 | {(ratings[ratingName] * 100).toFixed(0)}%
188 |
189 |
190 |
191 |
192 |
193 | );
194 | })}
195 |
196 | )
197 | })}
198 |
199 | )
200 | }
201 |
202 | export default RatingLens;
--------------------------------------------------------------------------------
/src/components/Lens/SpaceLens.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { GenerationProps } from "./Lens.types";
4 | import { ParameterProps } from "../Generator/Generator.types";
5 |
6 | import { ObjectsContext } from "../../context/ObjectsContextProvider";
7 |
8 | const SpaceLensContainer = styled.div`
9 | display: flex;
10 | flex-direction: row;
11 | gap: 8px;
12 | padding-right: 4px;
13 | width: 100%;
14 | height: 100%;
15 | `;
16 |
17 | const Space = styled.div`
18 | flex: 1;
19 | height: 100%;
20 | position: relative;
21 | `;
22 |
23 | const Dot = styled.div<{x: number, y: number, selected: boolean, viewed: boolean, hovered: boolean}>`
24 | position: absolute;
25 | top: ${({y}) => y}%;
26 | left: ${({x}) => x}%;
27 | width: 12px;
28 | height: 12px;
29 | border-radius: 50%;
30 | background-color: ${({viewed}) => viewed ? "#aaaaaa99" : "#0088ff66"};
31 | cursor: pointer;
32 | ${({hovered}) => hovered && `
33 | width: 16px;
34 | height: 16px;
35 | margin-left: -4px;
36 | margin-top: -4px;
37 | background-color: #0088ffaa;
38 | `}
39 | ${({selected}) => selected && `
40 | width: 16px;
41 | height: 16px;
42 | margin-left: -4px;
43 | margin-top: -4px;
44 | background-color: #0088ff;
45 | box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.2);
46 | `}
47 | &:hover {
48 | width: 16px;
49 | height: 16px;
50 | margin-left: -4px;
51 | margin-top: -4px;
52 | background-color: #0088ff;
53 | box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.2);
54 | }
55 | `;
56 |
57 | const InfoContainer = styled.div`
58 | display: flex;
59 | flex-direction: column;
60 | gap: 4px;
61 | flex: 1;
62 | height: 100%;
63 | width: 0;
64 | `;
65 |
66 | const InfoSection = styled.div`
67 | display: flex;
68 | padding: 8px;
69 | border-radius: 8px;
70 | font-size: 10px;
71 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.2);
72 | gap: 4px;
73 | flex-direction: column;
74 | overflow-y: auto;
75 | `;
76 |
77 | const InfoContent = styled.div`
78 | font-size: 12px;
79 | overflow-y: auto;
80 | flex: 1;
81 |
82 | &::-webkit-scrollbar {
83 | width: 4px;
84 | }
85 | &::-webkit-scrollbar-track {
86 | background: #f1f1f1;
87 | border-radius: 4px;
88 | }
89 | &::-webkit-scrollbar-thumb {
90 | background: #ddd;
91 | border-radius: 4px;
92 | }
93 | `;
94 |
95 | const AnimationContainer = styled.svg`
96 | position: absolute;
97 | top: calc(50% - 24px);
98 | left: calc(50% - 24px);
99 | height: 48px;
100 | width: 48px;
101 | z-index: 2;
102 | `;
103 |
104 | const parametersToHtml = (properties: ParameterProps[]) => {
105 | return properties.map((property: ParameterProps) => {
106 | const { name, value } = property;
107 | return (
108 |
109 | {name}: {value}
110 |
111 | )
112 | })
113 | }
114 |
115 | interface SpaceLensProps {
116 | generations: GenerationProps[];
117 | onGenerationClick?: (generationText: string) => void;
118 | getPosition: (generations: GenerationProps[]) => Promise<{x: number, y: number}[]>;
119 | }
120 |
121 | const SpaceLens: React.FC = ({
122 | generations,
123 | onGenerationClick,
124 | getPosition
125 | }) => {
126 | const { hoveredId, setHoveredId, updateGenerationsData } = React.useContext(ObjectsContext);
127 |
128 | const prevGenerations = React.useRef([]);
129 |
130 | const [selectedId, setSelectedId] = React.useState(null);
131 | const [viewed, setViewed] = React.useState([]);
132 |
133 | const [loadingCount, setLoadingCount] = React.useState(0);
134 |
135 | React.useEffect(() => {
136 | if(generations.length === prevGenerations.current.length) return;
137 |
138 | const newGenerations = generations.filter((generation: GenerationProps) => {
139 | return !prevGenerations.current.find((prevGeneration: GenerationProps) => prevGeneration.id === generation.id);
140 | });
141 |
142 | if(newGenerations.length === 0) return;
143 |
144 | const unprocessedGenerations = newGenerations.filter((generation: GenerationProps) => {
145 | const { metadata } = generation;
146 | if(!metadata) return true;
147 | const { position } = metadata;
148 | return !position;
149 | });
150 |
151 | setLoadingCount((prev) => prev + 1);
152 |
153 | getPosition(unprocessedGenerations).then((positions: {x: number, y: number}[]) => {
154 | if(positions.length == 0) {
155 | setLoadingCount((prev) => prev - 1);
156 | return;
157 | }
158 | updateGenerationsData(
159 | unprocessedGenerations.map((generation) => generation.id),
160 | positions.map((position) => ({position}))
161 | );
162 | setLoadingCount((prev) => prev - 1);
163 | });
164 |
165 | prevGenerations.current = generations;
166 | }, [generations]);
167 |
168 | const hoveredGeneration = generations.find((generation: GenerationProps) => generation.id === hoveredId);
169 | const selectedGeneration = generations.find((generation: GenerationProps) => generation.id === selectedId);
170 |
171 | const range = {x: {min: null, max: null}, y: {min: null, max: null}};
172 | generations.forEach((generation: GenerationProps) => {
173 | const { metadata } = generation;
174 | if(!metadata) return;
175 | const { position } = metadata;
176 | if(!position) return;
177 | const { x, y } = position;
178 | if(range.x.min == null || x < range.x.min) range.x.min = x;
179 | if(range.x.max == null || x > range.x.max) range.x.max = x;
180 | if(range.y.min == null || y < range.y.min) range.y.min = y;
181 | if(range.y.max == null || y > range.y.max) range.y.max = y;
182 | });
183 |
184 | return (
185 |
186 |
187 | {generations.length == 0 && (
188 |
189 | No generations yet...
190 |
191 | )}
192 | {loadingCount > 0 && (
193 |
194 |
195 |
198 |
199 |
200 |
201 |
202 | )}
203 | {generations.map((generation: GenerationProps, index: number) => {
204 | const { metadata } = generation;
205 | if(!metadata) return null;
206 | const { position } = metadata;
207 | if(!position) return null;
208 | const { x, y } = position;
209 |
210 | // normalize x and y
211 | const noise = (index % 15) / 5;
212 | const normalizedX = (range.x.min == null || range.x.max == null) ? 8 : (x - range.x.min) / (range.x.max - range.x.min) * 84 + 8 + noise;
213 | const normalizedY = (range.y.min == null || range.y.max == null) ? 8 : (y - range.y.min) / (range.y.max - range.y.min) * 84 + 8 + noise;
214 |
215 | return (
216 | setSelectedId(generation.id)}
220 | onMouseOver={() => {
221 | setHoveredId(generation.id);
222 | setViewed([...viewed, generation.id]);
223 | }}
224 | onMouseOut={() => setHoveredId(null)}
225 | selected={selectedId === generation.id}
226 | hovered={hoveredId === generation.id}
227 | viewed={viewed.includes(generation.id)}
228 | />
229 | )
230 | })}
231 |
232 |
233 |
234 | Input
235 |
236 | {hoveredGeneration ?
237 | hoveredGeneration.inputText :
238 | (selectedGeneration ? selectedGeneration.inputText : "Hover over a generation")
239 | }
240 |
241 |
242 |
243 | Parameters
244 |
245 | {hoveredGeneration ?
246 | parametersToHtml(hoveredGeneration.parameters) :
247 | (selectedGeneration ? parametersToHtml(selectedGeneration.parameters) : "Hover over a generation")
248 | }
249 |
250 |
251 | selectedGeneration && onGenerationClick && onGenerationClick(selectedGeneration.content)}
254 | >
255 | Output
256 |
257 | {hoveredGeneration ?
258 | hoveredGeneration.content :
259 | (selectedGeneration ? selectedGeneration.content : "Hover over a generation")
260 | }
261 |
262 |
263 |
264 |
265 | )
266 | }
267 |
268 | export default SpaceLens;
--------------------------------------------------------------------------------
/src/context/ObjectsContextProvider.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 |
4 | import { ObjectsContextProvider, ObjectsContext, IObjectsContext } from "./ObjectsContextProvider";
5 |
6 | import Cell from "../components/Cell/Cell";
7 | import Generator from "../components/Generator/Generator";
8 | import Lens from "../components/Lens/Lens";
9 | import { GeneratorProps } from "../components/Generator/Generator.types";
10 | import { LensProps, GenerationProps } from "../components/Lens/Lens.types";
11 |
12 | import getLoremIpsum from "../utils/getLoremIpsum";
13 |
14 | const meta: Meta = {
15 | component: ObjectsContextProvider,
16 | title: "tsook/ObjectsContextProvider",
17 | argTypes: {},
18 | };
19 | export default meta;
20 |
21 | type Story = StoryObj;
22 |
23 | const GeneratorArgs = [
24 | {
25 | id: "Generator-id",
26 | parameters: [
27 | {
28 | id: "model",
29 | name: "Model",
30 | value: "gpt-3.5-turbo",
31 | type: "nominal",
32 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "text-davinci-003"],
33 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "text-davinci-003": "D3"},
34 | defaultValue: "gpt-3.5-turbo"
35 | },
36 | {
37 | id: "temperature",
38 | name: "Temperature",
39 | nickname: "temp",
40 | value: 0.7,
41 | type: "continuous",
42 | allowedValues: [0.0, 2.0],
43 | defaultValue: 1.0
44 | },
45 | {
46 | id: "presence_penalty",
47 | name: "Presence Penalty",
48 | nickname: "presence",
49 | value: 0.0,
50 | type: "continuous",
51 | allowedValues: [0.0, 1.0],
52 | defaultValue: 0.0
53 | },
54 | {
55 | id: "top_k",
56 | name: "Top-K",
57 | nickname: "top",
58 | value: 3,
59 | type: "discrete",
60 | allowedValues: [1, 20],
61 | defaultValue: 0
62 | }
63 | ],
64 | color: "#0088ff",
65 | size: "large",
66 | isGenerating: false,
67 | isSelected: false,
68 | cellId: null,
69 | lensId: null
70 | },
71 | {
72 | id: "Generator2-id",
73 | parameters: [
74 | {
75 | id: "model",
76 | name: "Model",
77 | value: "gpt-3.5-turbo",
78 | type: "nominal",
79 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "text-davinci-003"],
80 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "text-davinci-003": "D3"},
81 | defaultValue: "gpt-3.5-turbo"
82 | },
83 | {
84 | id: "temperature",
85 | name: "Temperature",
86 | nickname: "temp",
87 | value: 0.7,
88 | type: "continuous",
89 | allowedValues: [0.0, 2.0],
90 | defaultValue: 1.0
91 | },
92 | {
93 | id: "presence_penalty",
94 | name: "Presence Penalty",
95 | nickname: "presence",
96 | value: 0.0,
97 | type: "continuous",
98 | allowedValues: [0.0, 1.0],
99 | defaultValue: 0.0
100 | },
101 | {
102 | id: "top_k",
103 | name: "Top-K",
104 | nickname: "top",
105 | value: 3,
106 | type: "discrete",
107 | allowedValues: [1, 20],
108 | defaultValue: 0
109 | }
110 | ],
111 | color: "#0088ff",
112 | size: "medium",
113 | isGenerating: false,
114 | isSelected: false,
115 | cellId: null,
116 | lensId: null
117 | },
118 | {
119 | id: "Generator3-id",
120 | parameters: [
121 | {
122 | id: "model",
123 | name: "Model",
124 | value: "gpt-3.5-turbo",
125 | type: "nominal",
126 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "text-davinci-003"],
127 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "text-davinci-003": "D3"},
128 | defaultValue: "gpt-3.5-turbo"
129 | },
130 | {
131 | id: "temperature",
132 | name: "Temperature",
133 | nickname: "temp",
134 | value: 0.7,
135 | type: "continuous",
136 | allowedValues: [0.0, 2.0],
137 | defaultValue: 1.0
138 | },
139 | {
140 | id: "presence_penalty",
141 | name: "Presence Penalty",
142 | nickname: "presence",
143 | value: 0.0,
144 | type: "continuous",
145 | allowedValues: [0.0, 1.0],
146 | defaultValue: 0.0
147 | },
148 | {
149 | id: "top_k",
150 | name: "Top-K",
151 | nickname: "top",
152 | value: 3,
153 | type: "discrete",
154 | allowedValues: [1, 20],
155 | defaultValue: 0
156 | }
157 | ],
158 | color: "#0088ff",
159 | size: "small",
160 | isGenerating: false,
161 | isSelected: false,
162 | cellId: null,
163 | lensId: null
164 | },
165 | ];
166 |
167 | const LensArgs = [
168 | {
169 | id: "Lens-id",
170 | type: "rating",
171 | style: {
172 | "width": "600px",
173 | "height": "200px",
174 | "border": "solid 2px #0088ff",
175 | "borderRadius": "8px",
176 | "boxShadow": "0px 4px 4px 2px rgba(0, 0, 0, 0.2)",
177 | "padding": "8px"
178 | },
179 | group: 0,
180 | getGenerationMetadata: (generations: GenerationProps[]) => {
181 | var ratings: {creative: number, engaging: number, positive: number, concise: number, simple: number}[] = [];
182 | for(let i = 0; i < generations.length; i++) {
183 | const creative = Math.floor(Math.random()*5);
184 | const engaging = Math.floor(Math.random()*10);
185 | const positive = Math.floor(Math.random()*10);
186 | const concise = Math.floor(Math.random()*8);
187 | const simple = Math.floor(Math.random()*40);
188 | ratings.push({creative, engaging, positive, concise, simple});
189 | }
190 | return new Promise((resolve) => {
191 | setTimeout(() => {
192 | resolve(ratings);
193 | }, 1000);
194 | }) as Promise<{[key: string]: number}[]>;
195 | }
196 | },
197 | {
198 | id: "Lens2-id",
199 | type: "space",
200 | style: {
201 | "width": "600px",
202 | "height": "200px",
203 | "border": "solid 2px #0088ff",
204 | "borderRadius": "8px",
205 | "boxShadow": "0px 4px 4px 2px rgba(0, 0, 0, 0.2)",
206 | "padding": "8px"
207 | },
208 | group: 0,
209 | getGenerationMetadata: (generations: GenerationProps[]) => {
210 | var positions: {x: number, y: number}[] = [];
211 | for(let i = 0; i < generations.length; i++) {
212 | const x = Math.random() - Math.random() * 3;
213 | const y = Math.random() + Math.random() * 5;
214 | positions.push({ x, y });
215 | }
216 | return new Promise((resolve) => {
217 | setTimeout(() => {
218 | resolve(positions);
219 | }, 1000);
220 | }) as Promise<{x: number, y: number}[]>;
221 | }
222 | },
223 | ]
224 |
225 | const ChildComponent = () => {
226 | const {
227 | cells,
228 | generators,
229 | lenses,
230 | addCell,
231 | linkCells,
232 | linkCellToGenerator,
233 | linkGeneratorToLens
234 | } = React.useContext(ObjectsContext);
235 |
236 | React.useEffect(() => {
237 | for(let i = 0; i < generators.length; i++) {
238 | for(let j = 0; j < lenses.length; j++) {
239 | linkGeneratorToLens(generators[i].id, lenses[j].id);
240 | }
241 | }
242 | }, []);
243 |
244 | const handleClick = () => {
245 | const data = {
246 | isActive: false,
247 | isMinimized: false,
248 | isReadonly: false,
249 | isSelected: false,
250 | parentCellId: null
251 | }
252 | var newId = addCell("Lorem Ipsum", data);
253 | if(cells.length > 0) {
254 | linkCells(newId, cells[cells.length-1].id);
255 | }
256 | for (let i = 0; i < generators.length; i++) {
257 | linkCellToGenerator(newId, generators[i].id);
258 | }
259 | }
260 |
261 | const [ text, setText ] = React.useState("");
262 | return (
263 |
264 |
265 | {cells.map((cell: any) => (
266 | |
270 | ))}
271 | Add
272 |
273 |
274 | {generators.map((generator: GeneratorProps) => (
275 |
279 | ))}
280 |
281 |
282 | {lenses.map((lens: LensProps) => (
283 | setText(text + " " + generation)}
287 | />
288 | ))}
289 |
290 |
291 |
293 |
294 | );
295 | }
296 |
297 | const generateLoremIpsum = (input: string | string[]) => {
298 | const startText = typeof input === "string" ? input : input.join(" ");
299 | const numSentences = Math.floor(Math.random() * 5) + 1;
300 | const output = getLoremIpsum(numSentences);
301 | return startText + " " + output;
302 | }
303 |
304 | export const Primary: Story = (args) => (
305 |
306 | );
307 | Primary.args = {
308 | children: ,
309 | cells: [],
310 | generators: GeneratorArgs,
311 | lenses: LensArgs,
312 | generateHandler: (input: string | string[], parameters: any) => {
313 | // wait 3 seconds
314 | return new Promise((resolve) => {
315 | setTimeout(() => {
316 | resolve(generateLoremIpsum(input));
317 | }, 3000);
318 | });
319 | }
320 | };
--------------------------------------------------------------------------------
/src/context/ObjectsContextProvider.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import '@testing-library/jest-dom';
3 | import userEvent from "@testing-library/user-event";
4 | import {render, screen, waitFor } from '@testing-library/react';
5 |
6 | import Generator from "../components/Generator/Generator";
7 | import Lens from "../components/Lens/Lens";
8 | import { ObjectsContextProvider, ObjectsContext, IObjectsContext } from "./ObjectsContextProvider";
9 |
10 | describe("ObjectsContextProvider", () => {
11 | var mockContextProvider: {
12 | generators: any[];
13 | lenses: any[];
14 | generateHandler: (input: string | string[], parameters: any) => Promise;
15 | } = {
16 | generators: [],
17 | lenses: [],
18 | generateHandler: async (input: string | string[], parameters: any) => {
19 | return "output";
20 | }
21 | };
22 |
23 | const MockChild = () => {
24 | const { generators, lenses } = React.useContext(ObjectsContext);
25 |
26 | return (
27 |
28 | {generators.map((generator: any) => (
29 |
33 | ))}
34 | {lenses.map((lens: any) => (
35 | {}}
39 | />
40 | ))}
41 |
42 | );
43 | }
44 |
45 | test("renders ObjectsContextProvider component", () => {
46 | render(
47 |
48 |
49 |
50 | );
51 | expect(screen.getByTestId("ChildComponent")).toBeInTheDocument();
52 | });
53 |
54 | mockContextProvider = {
55 | ...mockContextProvider,
56 | generators: [
57 | {
58 | id: "Generator-id",
59 | parameters: [
60 | {
61 | id: "model",
62 | name: "Model",
63 | value: "gpt-3.5-turbo",
64 | type: "nominal",
65 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "text-davinci-003"],
66 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "text-davinci-003": "D3"},
67 | defaultValue: "gpt-3.5-turbo"
68 | },
69 | {
70 | id: "temperature",
71 | name: "Temperature",
72 | nickname: "temp",
73 | value: 0.7,
74 | type: "continuous",
75 | allowedValues: [0.0, 2.0],
76 | defaultValue: 1.0
77 | },
78 | {
79 | id: "presence_penalty",
80 | name: "Presence Penalty",
81 | nickname: "presence",
82 | value: 0.0,
83 | type: "continuous",
84 | allowedValues: [0.0, 1.0],
85 | defaultValue: 0.0
86 | },
87 | {
88 | id: "top_k",
89 | name: "Top-K",
90 | nickname: "top",
91 | value: 3,
92 | type: "discrete",
93 | allowedValues: [1, 20],
94 | defaultValue: 0
95 | }
96 | ],
97 | color: "#0088ff",
98 | size: "large",
99 | isGenerating: false
100 | }
101 | ],
102 | lenses: [
103 | {
104 | id: "Lens-id",
105 | type: "list"
106 | }
107 | ]
108 | }
109 |
110 | test("renders ObjectsContextProvider component with generators", () => {
111 | render(
112 |
113 |
114 |
115 | );
116 | expect(screen.getByText("Generate")).toBeInTheDocument();
117 | });
118 | });
--------------------------------------------------------------------------------
/src/context/Storywriting.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Meta, StoryObj } from "@storybook/react";
3 |
4 | import { ObjectsContextProvider, ObjectsContext, IObjectsContext } from "./ObjectsContextProvider";
5 |
6 | import CellTree from "../components/Cell/CellTree";
7 | import CellEditor from "../components/Cell/CellEditor";
8 |
9 | import Generator from "../components/Generator/Generator";
10 | import { GeneratorProps } from "../components/Generator/Generator.types";
11 |
12 | import ListLens from "../components/Lens/ListLens";
13 | import SpaceLens from "../components/Lens/SpaceLens";
14 | import { LensProps, GenerationProps } from "../components/Lens/Lens.types";
15 |
16 | const meta: Meta = {
17 | component: ObjectsContextProvider,
18 | title: "tsook/Storywriting",
19 | argTypes: {},
20 | };
21 | export default meta;
22 |
23 | type Story = StoryObj;
24 |
25 | const ChildComponent = () => {
26 | const { cells, generators, lenses } = React.useContext(ObjectsContext);
27 |
28 | const activePath = cells.filter((cell) => cell.isActive).map((cell) => cell.id);
29 | const hoveredPath = cells.filter((cell) => cell.isHovered).map((cell) => cell.id);
30 |
31 | return (
32 |
33 | 0 ? hoveredPath : activePath}
35 | style={{width: "50%", height: "100%"}}
36 | textColor={hoveredPath.length > 0 ? "#0066ff99" : undefined}
37 | />
38 |
43 |
44 | )
45 | }
46 |
47 | const CellArgs = [
48 | {
49 | id: "0",
50 | text: "Start of the story.",
51 | isActive: true,
52 | minimizedText: "start",
53 | parentCellId: null
54 | },
55 | {
56 | id: "1",
57 | text: "Second sentence of the story.",
58 | isActive: true,
59 | minimizedText: "second",
60 | parentCellId: "0"
61 | },
62 | {
63 | id: "2",
64 | text: "Alternative second sentence of the story.",
65 | isActive: false,
66 | minimizedText: "alternative",
67 | parentCellId: "0"
68 | },
69 | {
70 | id: "3",
71 | text: "Third sentence of the story.",
72 | isActive: true,
73 | minimizedText: "third",
74 | parentCellId: "1"
75 | }
76 | ];
77 |
78 | const DefaultParameters = [
79 | {
80 | id: "model",
81 | name: "Model",
82 | value: "gpt-3.5-turbo",
83 | type: "nominal",
84 | allowedValues: ["gpt-3.5-turbo", "gpt-4", "text-davinci-003"],
85 | valueNicknames: {"gpt-3.5-turbo": "3.5", "gpt-4": "4", "text-davinci-003": "D3"},
86 | defaultValue: "gpt-3.5-turbo"
87 | },
88 | {
89 | id: "temperature",
90 | name: "Temperature",
91 | nickname: "temp",
92 | value: 0.7,
93 | type: "continuous",
94 | allowedValues: [0.0, 2.0],
95 | defaultValue: 1.0
96 | },
97 | {
98 | id: "presence_penalty",
99 | name: "Presence Penalty",
100 | nickname: "presence",
101 | value: 0.0,
102 | type: "continuous",
103 | allowedValues: [0.0, 1.0],
104 | defaultValue: 0.0
105 | },
106 | {
107 | id: "top_k",
108 | name: "Top-K",
109 | nickname: "top",
110 | value: 3,
111 | type: "discrete",
112 | allowedValues: [1, 20],
113 | defaultValue: 0
114 | }
115 | ];
116 | const GeneratorArgs = [
117 | {
118 | id: "generator-0",
119 | parameters: JSON.parse(JSON.stringify(DefaultParameters)),
120 | color: "#0066ff",
121 | cellId: null,
122 | lensId: null
123 | }
124 | ];
125 |
126 | const LensArgs = [
127 | {
128 | id: "Lens-id",
129 | type: "list",
130 | generationIds: [],
131 | style: {
132 | "width": "600px",
133 | "height": "400px",
134 | "border": "solid 2px #0088ff",
135 | "borderRadius": "8px",
136 | "boxShadow": "0px 4px 4px 2px rgba(0, 0, 0, 0.2)",
137 | "padding": "8px"
138 | },
139 | group: 0
140 | }
141 | ];
142 |
143 | export const Primary: Story = (args) => (
144 |
145 | );
146 | Primary.args = {
147 | children: ,
148 | cells: CellArgs,
149 | generators: GeneratorArgs,
150 | lenses: LensArgs,
151 | generateHandler: (input: string | string[], parameters: any) => {
152 | // wait 3 seconds
153 | return new Promise((resolve) => {
154 | setTimeout(() => {
155 | resolve(input);
156 | }, 3000);
157 | });
158 | },
159 | minimizeHandler: (text: string) => {
160 | const words = text.split(" ").map((word) => word.replace(/[.,?\/#!$%\^&\*;:{}=\-_`~()]/g,""));
161 | const longestWord = words.reduce((a, b) => a.length > b.length ? a : b);
162 | return longestWord;
163 | }
164 | };
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Cell from './components/Cell/Cell';
2 | import { CellProps } from './components/Cell/Cell.types';
3 | import CellBoard from './components/Cell/CellBoard';
4 | import CellTree from './components/Cell/CellTree';
5 | import CellEditor from './components/Cell/CellEditor';
6 |
7 | import Generator from './components/Generator/Generator';
8 | import { GeneratorProps, ParameterProps } from './components/Generator/Generator.types';
9 |
10 | import Lens from './components/Lens/Lens';
11 | import { LensProps, GenerationProps } from './components/Lens/Lens.types';
12 | import ListLens from './components/Lens/ListLens';
13 | import SpaceLens from './components/Lens/SpaceLens';
14 |
15 | import { ObjectsContext, ObjectsContextProvider } from './context/ObjectsContextProvider';
16 |
17 | export type {
18 | CellProps,
19 | GeneratorProps,
20 | ParameterProps,
21 | LensProps,
22 | GenerationProps
23 | }
24 |
25 | export {
26 | Cell,
27 | CellBoard,
28 | CellTree,
29 | CellEditor,
30 | Generator,
31 | Lens,
32 | ListLens,
33 | SpaceLens,
34 | ObjectsContext,
35 | ObjectsContextProvider
36 | }
--------------------------------------------------------------------------------
/src/utils/getLoremIpsum.tsx:
--------------------------------------------------------------------------------
1 | const loremIpsum = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eget massa venenatis ligula dictum interdum. Phasellus varius cursus arcu, aliquet suscipit nunc mollis nec. Etiam mattis ligula nibh. Suspendisse viverra aliquam luctus. Fusce faucibus nisi in augue iaculis, ut tincidunt lorem laoreet. Sed convallis leo mi, nec auctor nibh pharetra sit amet. Nullam lobortis mollis sapien, ut dignissim ligula lacinia ac. Vestibulum scelerisque lectus ut est tincidunt, non mollis nunc rhoncus. Cras elit diam, vulputate eget turpis vel, finibus sodales eros. Quisque in efficitur ante. Quisque efficitur consequat sollicitudin. Ut maximus justo ut nunc tincidunt sodales. In hac habitasse platea dictumst. Vivamus lacinia ipsum egestas tellus sollicitudin ullamcorper. Nunc eu nunc eget nisi rhoncus scelerisque. In ut venenatis tellus, in convallis mauris. Nulla hendrerit sollicitudin velit, ac auctor turpis commodo id. Donec egestas odio a risus molestie, vitae cursus magna venenatis. Morbi pulvinar congue dui, sed vulputate magna gravida sit amet. Suspendisse leo dolor, imperdiet eu nunc eu, suscipit imperdiet velit. Phasellus blandit pretium tellus et imperdiet. Praesent malesuada risus id vehicula ultricies. Fusce in lectus a mauris feugiat maximus nec quis ante. Nulla ultrices pulvinar mauris, in rutrum massa malesuada eget. Phasellus pharetra vestibulum diam, ac porta augue. Suspendisse potenti. Suspendisse sed justo at nisi faucibus volutpat. Duis ut sapien massa. Donec ac tincidunt ante. In egestas ex lacus. Aenean vel nibh commodo quam pulvinar tempus. Duis imperdiet, ligula et ultrices mollis, nibh est ullamcorper libero, sed efficitur augue ligula a quam. Vivamus iaculis, erat nec accumsan efficitur, ante ligula ullamcorper purus, vel condimentum dui enim quis neque. Integer leo ante, molestie in tristique quis, pretium sed est. Maecenas non urna nibh. Maecenas non ipsum tincidunt, aliquam eros nec, congue ante. Praesent sit amet nunc nec massa accumsan dapibus. Fusce lobortis mollis lectus, sit amet porttitor neque ullamcorper ut. Donec in ligula ut eros rhoncus varius. Quisque sed lectus sit amet odio porta feugiat consectetur at justo. Ut suscipit porttitor aliquet. Ut ornare lacus ac eros convallis mattis at at nunc. Morbi vitae elementum enim. Pellentesque gravida luctus ipsum, id pellentesque nisl eleifend sed. Maecenas consequat nisi vel arcu facilisis semper eu rutrum turpis. Maecenas vel lectus lobortis, volutpat eros eu, luctus neque. Curabitur id elit ac nisi tempus aliquet. Nullam tincidunt ante eget viverra iaculis. Morbi sodales odio vel neque lobortis pharetra. Maecenas sed dolor vel mi aliquam faucibus. Curabitur nec magna sem.`
2 |
3 | const getLoremIpsum = (numSentences: number) => {
4 | // get random sentences from loremIpsum
5 | const sentences = loremIpsum.split(". ");
6 | const randomSentences = [];
7 | for (let i = 0; i < numSentences; i++) {
8 | randomSentences.push(sentences[Math.floor(Math.random() * sentences.length)]);
9 | }
10 | return randomSentences.join(". ") + ".";
11 | }
12 |
13 | export default getLoremIpsum;
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "strict": true,
5 | "skipLibCheck": true,
6 | "jsx": "react",
7 | "module": "ESNext",
8 | "declaration": true,
9 | "declarationDir": "types",
10 | "sourceMap": true,
11 | "outDir": "dist",
12 | "moduleResolution": "node",
13 | "emitDeclarationOnly": true,
14 | "allowSyntheticDefaultImports": true,
15 | "forceConsistentCasingInFileNames": true
16 | },
17 | "exclude": [
18 | "dist",
19 | "node_modules",
20 | "src/**/*.test.tsx",
21 | "src/**/*.stories.tsx",
22 | "demo/**/*"
23 | ]
24 | }
--------------------------------------------------------------------------------
|