From d9c1282d994be7a97b8a55c359a4d5ec79724a81 Mon Sep 17 00:00:00 2001 From: Jordan Date: Wed, 28 Feb 2024 08:36:40 -0800 Subject: [PATCH] prompts can be added to prompt composer. --- package.json | 8 ++- pnpm-lock.yaml | 15 +++++ src/App.tsx | 5 +- src/components/IComposable.tsx | 5 -- src/components/LibraryItem.tsx | 3 +- src/components/NewLibraryItem.test.tsx | 77 ++++++++++++++++++++++++ src/components/NewLibraryItem.tsx | 81 ++++++++++++++++++++++++++ src/components/Nugget.tsx | 3 +- src/components/Operation.tsx | 5 +- src/components/PromptComposer.tsx | 56 +++++++++++------- src/components/PromptLibrary.css | 3 + src/components/PromptLibrary.test.tsx | 1 + src/components/PromptLibrary.tsx | 46 +++++++++++---- src/components/TextPrompt.css | 6 ++ src/components/TextPrompt.tsx | 11 ++++ src/index.tsx | 6 +- src/lib/ast.ts | 2 +- src/lib/prompt.test.ts | 2 +- src/lib/prompt.tsx | 7 ++- 19 files changed, 283 insertions(+), 59 deletions(-) delete mode 100644 src/components/IComposable.tsx create mode 100644 src/components/NewLibraryItem.test.tsx create mode 100644 src/components/NewLibraryItem.tsx create mode 100644 src/components/PromptLibrary.css create mode 100644 src/components/TextPrompt.css create mode 100644 src/components/TextPrompt.tsx diff --git a/package.json b/package.json index eee1d9a..cc6f730 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "react-redux": "^9.1.0", "react-scripts": "5.0.1", "typescript": "^4.9.5", + "uuid": "^9.0.1", "vite": "^5.1.4", "web-vitals": "^2.1.4" }, @@ -57,11 +58,12 @@ "devDependencies": { "@testing-library/jest-dom": "^4.2.4", "@types/jest": "^27.5.2", + "@types/uuid": "^9.0.8", "jest-without-globals": "^0.0.3" }, "jest": { - "transformIgnorePatterns": [ - "!node_modules/" - ] + "transformIgnorePatterns": [ + "!node_modules/" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4598a03..ba07f93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ dependencies: typescript: specifier: ^4.9.5 version: 4.9.5 + uuid: + specifier: ^9.0.1 + version: 9.0.1 vite: specifier: ^5.1.4 version: 5.1.4(@types/node@16.18.82) @@ -94,6 +97,9 @@ devDependencies: '@types/jest': specifier: ^27.5.2 version: 27.5.2 + '@types/uuid': + specifier: ^9.0.8 + version: 9.0.8 jest-without-globals: specifier: ^0.0.3 version: 0.0.3(jest@25.5.4) @@ -3816,6 +3822,10 @@ packages: resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} dev: false + /@types/uuid@9.0.8: + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + dev: true + /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: @@ -12908,6 +12918,11 @@ packages: hasBin: true dev: false + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + /v8-to-istanbul@4.1.4: resolution: {integrity: sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==} engines: {node: 8.x.x || >=10.10.0} diff --git a/src/App.tsx b/src/App.tsx index 695b0d2..bf01b0e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import './App.css'; import PromptComposer from './components/PromptComposer'; import { Box, Tabs, Tab, Typography } from '@material-ui/core'; import { $textComposition } from './lib/prompt'; +import { TextPrompt } from './components/TextPrompt'; interface TabPanelProps { @@ -56,9 +57,7 @@ function App() { -
- -
+
); diff --git a/src/components/IComposable.tsx b/src/components/IComposable.tsx deleted file mode 100644 index 1742902..0000000 --- a/src/components/IComposable.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Component, ReactComponentElement, ReactInstance, ReactNode } from "react"; - -export type Composable = { - -} & ReactNode \ No newline at end of file diff --git a/src/components/LibraryItem.tsx b/src/components/LibraryItem.tsx index 170b8ee..b7e2447 100644 --- a/src/components/LibraryItem.tsx +++ b/src/components/LibraryItem.tsx @@ -1,4 +1,3 @@ -import { Composable } from "./IComposable"; import { LibraryItem as LibItem } from "../lib/prompt"; import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined'; import { Button } from "@material-ui/core"; @@ -23,5 +22,5 @@ export function LibraryItem(props: StyleProps) { } - ) as Composable; + ); }; \ No newline at end of file diff --git a/src/components/NewLibraryItem.test.tsx b/src/components/NewLibraryItem.test.tsx new file mode 100644 index 0000000..eb18e41 --- /dev/null +++ b/src/components/NewLibraryItem.test.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { render, screen, fireEvent, act } from '@testing-library/react'; +import {NewLibraryItem} from './NewLibraryItem'; +import { Category, addItemToLibrary, categoryHasName } from '../lib/prompt'; + +jest.mock('../lib/prompt', () => ({ + Category: Category, + addItemToLibrary: jest.fn(), + categoryHasName: jest.fn(), +})); + +describe('NewLibraryItem', () => { + beforeEach(() => { + (addItemToLibrary as jest.Mock).mockReset(); + (categoryHasName as jest.Mock).mockReset(); + }); + + it('renders the form', () => { + render(); + + expect(screen.getByText('Category')).toBeInTheDocument(); + expect(screen.getByText('Subject')).toBeInTheDocument(); + expect(screen.getByText('Prompt')).toBeInTheDocument(); + expect(screen.getByText('Create')).toBeInTheDocument(); + }); + + it('changes the category', () => { + render(); + + fireEvent.change(screen.getByLabelText('Prompt Item Category'), { + target: { value: Category.vibes }, + }); + + expect(screen.getByLabelText('Prompt Item Name')).toBeVisible(); + }); + + it('changes the name', () => { + render(); + + fireEvent.change(screen.getByLabelText('Name'), { + target: { value: 'Test' }, + }); + + expect(screen.getByDisplayValue('Test')).toBeInTheDocument(); + }); + + it('changes the prompt', () => { + render(); + + fireEvent.change(screen.getByLabelText('Prompt'), { + target: { value: 'Test' }, + }); + + expect(screen.getByDisplayValue('Test')).toBeInTheDocument(); + }); + + it('creates a new library item', () => { + render(); + + fireEvent.change(screen.getByLabelText('Category'), { + target: { value: Category.subject }, + }); + fireEvent.change(screen.getByLabelText('Name'), { + target: { value: 'Test' }, + }); + fireEvent.change(screen.getByLabelText('Prompt'), { + target: { value: 'Test' }, + }); + fireEvent.click(screen.getByText('Create')); + + expect(addItemToLibrary).toHaveBeenCalledWith({ + category: Category.subject, + name: 'Test', + prompt: 'Test', + }); + }); +}); diff --git a/src/components/NewLibraryItem.tsx b/src/components/NewLibraryItem.tsx new file mode 100644 index 0000000..0b68081 --- /dev/null +++ b/src/components/NewLibraryItem.tsx @@ -0,0 +1,81 @@ +import { Button, FormControl, InputLabel, MenuItem, TextField } from "@material-ui/core"; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import { Category, LibraryItem, addItemToLibrary, categoryHasName } from "../lib/prompt"; +import { ChangeEvent, useState } from "react"; +import {v4 as uuidv4} from "uuid" + +export interface NewLibraryItemProps { + onNewCreated?: () => void; +} + +export function NewLibraryItem(props: NewLibraryItemProps) { + const { onNewCreated } = props; + + const [category, setCategory] = useState(Category.subject); + const [name, setName] = useState(""); + const [prompt, setPrompt] = useState(""); + + const handleCategoryChange = (e: any | SelectChangeEvent) => { + setCategory(e.target.value as Category); + } + const handleNameChange = (e: ChangeEvent) => { + setName(e.target.value); + } + const handlePromptChange = (e: ChangeEvent) => { + setPrompt(e.target.value); + } + + const titleCase = (s: string) => { + return s[0].toUpperCase() + s.substring(1); + } + + const handleCreateItem = () => { + const libraryItem = { + id: uuidv4(), + category, + name: categoryHasName(category) ? name : null, + prompt, + } as LibraryItem; + addItemToLibrary(libraryItem); + setCategory(Category.subject); + setName(""); + setPrompt(""); + onNewCreated ? onNewCreated() : null; + } + + const catChoices = Object.keys(Category); + + return ( +
+
+ + Category + + +
+
+ + {categoryHasName(category) ? (Name) : <>} + {categoryHasName(category) ? () : <>} + +
+
+ + Prompt + + + +
+
+ ) +} \ No newline at end of file diff --git a/src/components/Nugget.tsx b/src/components/Nugget.tsx index 6c1a57b..f9221b4 100644 --- a/src/components/Nugget.tsx +++ b/src/components/Nugget.tsx @@ -2,7 +2,6 @@ import { Button, ButtonGroup, Chip, Divider } from '@material-ui/core'; import React, { Component, useState } from 'react'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowUp'; -import {Composable} from "./IComposable" import "./Nugget.css"; import { Nugget as NuggetType, decreaseNuggetScore, increaseNuggetScore } from '../lib/prompt'; @@ -32,5 +31,5 @@ export default function Nugget(props : NuggetProps) { - ) as Composable; + ); } \ No newline at end of file diff --git a/src/components/Operation.tsx b/src/components/Operation.tsx index 98f3c74..9b91081 100644 --- a/src/components/Operation.tsx +++ b/src/components/Operation.tsx @@ -2,8 +2,7 @@ import { Menu, MenuItem } from "@material-ui/core"; import React, { Children, ReactNode } from 'react'; import "./Operation.css"; import { Op } from "../lib/operator"; -import { randomUUID } from "crypto"; -import { Composable } from "./IComposable"; +import { v4 as randomUUID } from "uuid"; import { Operation as OperationType, changeOperationOp } from "../lib/prompt"; import Nugget from "./Nugget"; @@ -72,7 +71,7 @@ function Operation(props : OperationProps) { })} - ) as Composable; + ); } export { Operation, Op }; \ No newline at end of file diff --git a/src/components/PromptComposer.tsx b/src/components/PromptComposer.tsx index f9fb588..a52cd28 100644 --- a/src/components/PromptComposer.tsx +++ b/src/components/PromptComposer.tsx @@ -1,27 +1,29 @@ import { Button } from '@material-ui/core'; import Masonry from '@mui/lab/Masonry'; -import Nugget from './Nugget'; -import { Operation } from './Operation'; import AddIcon from '@mui/icons-material/Add'; -import "./PromptArea.css"; +import "./PromptComposer.css"; import { PromptLibrary } from './PromptLibrary'; -import React from 'react'; -import { $composition, $slottedComposition, LibraryItem, insertIntoComposition } from '../lib/prompt'; +import React, { useState } from 'react'; +import { $slottedComposition, LibraryItem, PromptItem, insertIntoComposition } from '../lib/prompt'; import { Category } from '@mui/icons-material'; import { useStore } from '@nanostores/react' +import Nugget from './Nugget'; +import { Stack } from '@mui/material'; +import { Operation } from './Operation'; -type Composable = (typeof Nugget) | (typeof Operation); +export interface PromptComposerProps { -export default function PromptComposer(props) { - const [open, setOpen] = React.useState(false); +} + +export default function PromptComposer(props: PromptComposerProps) { + const [open, setOpen] = useState(false); const handleClickOpen = () => { setOpen(true); }; - const handleClose = (value: string) => { + const handleClose = () => { setOpen(false); - // setSelectedValue(value); }; const handleOnInsertItem = (item: LibraryItem) => { @@ -30,19 +32,31 @@ export default function PromptComposer(props) { const slottedComposition = useStore($slottedComposition); + const promptItemFactory = (promptItem : PromptItem, key : string) => { + return "op" in promptItem ? : + } + return (
- - -
Something
-
+
+ +
+
+ { + slottedComposition.map((itemCol, i) => ( + + {itemCol.map((promptItem, j) => promptItemFactory(promptItem, `item-${j}-${j}`))} + + )) + } +
); } \ No newline at end of file diff --git a/src/components/PromptLibrary.css b/src/components/PromptLibrary.css new file mode 100644 index 0000000..90f8ea4 --- /dev/null +++ b/src/components/PromptLibrary.css @@ -0,0 +1,3 @@ +.prompt-library-dialog .categories div { + display: inline; +} \ No newline at end of file diff --git a/src/components/PromptLibrary.test.tsx b/src/components/PromptLibrary.test.tsx index 57d1b50..6fa3267 100644 --- a/src/components/PromptLibrary.test.tsx +++ b/src/components/PromptLibrary.test.tsx @@ -31,6 +31,7 @@ const mockOpen: boolean = true; const mockProps: SimpleDialogProps = { open: mockOpen, onInsertItem: mockOnAddItem, + onClose: mockOnClose, }; const mockLibrary: LibraryType = [ diff --git a/src/components/PromptLibrary.tsx b/src/components/PromptLibrary.tsx index 33cd611..7adb887 100644 --- a/src/components/PromptLibrary.tsx +++ b/src/components/PromptLibrary.tsx @@ -1,12 +1,15 @@ -import { Checkbox, Dialog, DialogTitle } from "@material-ui/core"; +import { Button, Checkbox, Dialog, DialogTitle } from "@material-ui/core"; import { LibraryItem as LibItemType, $library, Category } from "../lib/prompt"; import { LibraryItem } from "./LibraryItem"; -import { ChangeEvent } from "react"; +import { ChangeEvent, useState } from "react"; import { useStore } from "@nanostores/react"; +import { ExitToAppOutlined } from "@mui/icons-material"; +import { NewLibraryItem } from "./NewLibraryItem"; +import "./PromptLibrary.css" export interface SimpleDialogProps { open: boolean; - // onClose: (composable: Composable) => void, + onClose: () => void, // onAddItem: (item: LibItemType) => void, onInsertItem: (item: LibItemType) => void, } @@ -25,10 +28,14 @@ const show = ($el: Element) => { } export function PromptLibrary(props: SimpleDialogProps) { - const { open, onInsertItem } = props; + const { open, onInsertItem, onClose } = props; const library = useStore($library); + const [doCreate, setDoCreate] = useState(false); + + const [visibleCategories, setVisibleCategories] = useState([] as Category[]); + const handleOnAddItem = (item: LibItemType) => { // onAddItem(item); } @@ -37,32 +44,47 @@ export function PromptLibrary(props: SimpleDialogProps) { onInsertItem(item); } - const filterCat = (catKey: string, catVal: string, v: ChangeEvent) => { + const filterCat = (catKey: string, v: ChangeEvent) => { const isChecked = v.target.value === '1'; - document.querySelectorAll(`.category-${catVal}`).forEach($el => { + setVisibleCategories((visibleCategories.includes(catKey as Category) && !isChecked) ? visibleCategories.filter(c => c != catKey) : [...visibleCategories, catKey as Category]); + document.querySelectorAll(`.category-${catKey}`).forEach($el => { if (isChecked) show($el) else hide($el) }); } + const handleClose = () => { + onClose(); + } + + const handleOnNewCreated = () => { + setDoCreate(false); + } + + const categoryChoices = Object.keys(Category); + return ( - + Prompt Library
- {Object.entries(Category).map(([catKey, catVal]) => { + {categoryChoices.map(catKey => { return ( -
- filterCat(catKey, catVal, v)} /> - {title(catVal)} +
+ filterCat(catKey, v)} /> + {title(catKey)}
) })}
{ - library?.map(item => ) + library?.map(item => ) }
+
+ +
+ {doCreate ? () : (<>)}
); } \ No newline at end of file diff --git a/src/components/TextPrompt.css b/src/components/TextPrompt.css new file mode 100644 index 0000000..cae6f2f --- /dev/null +++ b/src/components/TextPrompt.css @@ -0,0 +1,6 @@ +.text-prompt { + width: 75%; + height: 75%; + padding: 10pt; + margin: 4pt; +} \ No newline at end of file diff --git a/src/components/TextPrompt.tsx b/src/components/TextPrompt.tsx new file mode 100644 index 0000000..546c5bb --- /dev/null +++ b/src/components/TextPrompt.tsx @@ -0,0 +1,11 @@ +import { TextareaAutosize } from "@material-ui/core"; +import { $textComposition } from "../lib/prompt"; +import "./TextPrompt.css"; +import { useStore } from "@nanostores/react"; + +export function TextPrompt () { + const text = useStore($textComposition); + return ( + + ) +} \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index c079c85..032464f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,17 +3,13 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; -import { Provider } from 'react-redux'; -import store from "./store" const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( - - - + ); diff --git a/src/lib/ast.ts b/src/lib/ast.ts index ef62b21..b487ca1 100644 --- a/src/lib/ast.ts +++ b/src/lib/ast.ts @@ -1,4 +1,4 @@ -import { randomUUID } from "crypto"; +import { v4 as randomUUID } from "uuid"; import { Op } from "./operator"; import { createSlice } from '@reduxjs/toolkit' diff --git a/src/lib/prompt.test.ts b/src/lib/prompt.test.ts index 8397fcd..7dc1b85 100644 --- a/src/lib/prompt.test.ts +++ b/src/lib/prompt.test.ts @@ -1,4 +1,4 @@ -import { randomUUID } from "crypto"; +import { v4 as randomUUID } from "uuid"; import { Op } from "./operator"; import { Library as LibraryType, diff --git a/src/lib/prompt.tsx b/src/lib/prompt.tsx index 7c62686..0500e46 100644 --- a/src/lib/prompt.tsx +++ b/src/lib/prompt.tsx @@ -1,4 +1,4 @@ -import { randomUUID } from "crypto"; +import { v4 as randomUUID } from "uuid"; import { Op } from "./operator"; import { atom, computed } from "nanostores"; @@ -8,6 +8,7 @@ type IdAble = { id: Id, } +// only vibes and styles will have a name. export enum Category { subject = "subject", style = "style", @@ -15,6 +16,10 @@ export enum Category { medium = "medium", } +export function categoryHasName(cat : Category) { + return (cat === Category.style || cat === Category.vibes) +} + const N_CATEGORIES = Object.keys(Category).length; export function catI(c: Category | string): number {