prompts can be added to prompt composer.

This commit is contained in:
Jordan 2024-02-28 08:36:40 -08:00
parent b0c70f6d17
commit d9c1282d99
19 changed files with 283 additions and 59 deletions

View File

@ -27,6 +27,7 @@
"react-redux": "^9.1.0", "react-redux": "^9.1.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"uuid": "^9.0.1",
"vite": "^5.1.4", "vite": "^5.1.4",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
@ -57,11 +58,12 @@
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/uuid": "^9.0.8",
"jest-without-globals": "^0.0.3" "jest-without-globals": "^0.0.3"
}, },
"jest": { "jest": {
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"!node_modules/" "!node_modules/"
] ]
} }
} }

View File

@ -80,6 +80,9 @@ dependencies:
typescript: typescript:
specifier: ^4.9.5 specifier: ^4.9.5
version: 4.9.5 version: 4.9.5
uuid:
specifier: ^9.0.1
version: 9.0.1
vite: vite:
specifier: ^5.1.4 specifier: ^5.1.4
version: 5.1.4(@types/node@16.18.82) version: 5.1.4(@types/node@16.18.82)
@ -94,6 +97,9 @@ devDependencies:
'@types/jest': '@types/jest':
specifier: ^27.5.2 specifier: ^27.5.2
version: 27.5.2 version: 27.5.2
'@types/uuid':
specifier: ^9.0.8
version: 9.0.8
jest-without-globals: jest-without-globals:
specifier: ^0.0.3 specifier: ^0.0.3
version: 0.0.3(jest@25.5.4) version: 0.0.3(jest@25.5.4)
@ -3816,6 +3822,10 @@ packages:
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
dev: false dev: false
/@types/uuid@9.0.8:
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
dev: true
/@types/ws@8.5.10: /@types/ws@8.5.10:
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
dependencies: dependencies:
@ -12908,6 +12918,11 @@ packages:
hasBin: true hasBin: true
dev: false dev: false
/uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
dev: false
/v8-to-istanbul@4.1.4: /v8-to-istanbul@4.1.4:
resolution: {integrity: sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==} resolution: {integrity: sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==}
engines: {node: 8.x.x || >=10.10.0} engines: {node: 8.x.x || >=10.10.0}

View File

@ -3,6 +3,7 @@ import './App.css';
import PromptComposer from './components/PromptComposer'; import PromptComposer from './components/PromptComposer';
import { Box, Tabs, Tab, Typography } from '@material-ui/core'; import { Box, Tabs, Tab, Typography } from '@material-ui/core';
import { $textComposition } from './lib/prompt'; import { $textComposition } from './lib/prompt';
import { TextPrompt } from './components/TextPrompt';
interface TabPanelProps { interface TabPanelProps {
@ -56,9 +57,7 @@ function App() {
<PromptComposer /> <PromptComposer />
</CustomTabPanel> </CustomTabPanel>
<CustomTabPanel value={value} index={1}> <CustomTabPanel value={value} index={1}>
<div> <TextPrompt />
<textarea aria-label='text-area'>{ $textComposition.get() }</textarea>
</div>
</CustomTabPanel> </CustomTabPanel>
</> </>
); );

View File

@ -1,5 +0,0 @@
import { Component, ReactComponentElement, ReactInstance, ReactNode } from "react";
export type Composable = {
} & ReactNode

View File

@ -1,4 +1,3 @@
import { Composable } from "./IComposable";
import { LibraryItem as LibItem } from "../lib/prompt"; import { LibraryItem as LibItem } from "../lib/prompt";
import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined'; import AddCircleOutlineOutlinedIcon from '@mui/icons-material/AddCircleOutlineOutlined';
import { Button } from "@material-ui/core"; import { Button } from "@material-ui/core";
@ -23,5 +22,5 @@ export function LibraryItem(props: StyleProps) {
} }
</span> </span>
</div> </div>
) as Composable; );
}; };

View File

@ -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(<NewLibraryItem />);
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(<NewLibraryItem />);
fireEvent.change(screen.getByLabelText('Prompt Item Category'), {
target: { value: Category.vibes },
});
expect(screen.getByLabelText('Prompt Item Name')).toBeVisible();
});
it('changes the name', () => {
render(<NewLibraryItem />);
fireEvent.change(screen.getByLabelText('Name'), {
target: { value: 'Test' },
});
expect(screen.getByDisplayValue('Test')).toBeInTheDocument();
});
it('changes the prompt', () => {
render(<NewLibraryItem />);
fireEvent.change(screen.getByLabelText('Prompt'), {
target: { value: 'Test' },
});
expect(screen.getByDisplayValue('Test')).toBeInTheDocument();
});
it('creates a new library item', () => {
render(<NewLibraryItem />);
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',
});
});
});

View File

@ -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<HTMLInputElement | HTMLTextAreaElement>) => {
setName(e.target.value);
}
const handlePromptChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
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 (
<div>
<div>
<FormControl>
<InputLabel htmlFor="new-prompt-category">Category</InputLabel>
<Select
native
id="new-prompt-category"
aria-label="Prompt Item Category"
value={category}
onChange={(e) => handleCategoryChange(e)}
>
{catChoices.map(cat => (
<option value={cat} id={cat} key={cat}>{titleCase(cat)}</option>
))}
</Select>
</FormControl>
</div>
<div>
<FormControl>
{categoryHasName(category) ? (<InputLabel htmlFor="name">Name</InputLabel>) : <></>}
{categoryHasName(category) ? (<TextField aria-label="Prompt Item Name" value={name} onChange={handleNameChange} id="name" />) : <></>}
</FormControl>
</div>
<div>
<FormControl>
<InputLabel htmlFor="prompt">Prompt</InputLabel>
<TextField aria-label="Prompt Item Text" value={prompt} onChange={handlePromptChange} id="prompt" />
<Button onClick={handleCreateItem} >Create</Button>
</FormControl>
</div>
</div>
)
}

View File

@ -2,7 +2,6 @@ import { Button, ButtonGroup, Chip, Divider } from '@material-ui/core';
import React, { Component, useState } from 'react'; import React, { Component, useState } from 'react';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowUp';
import {Composable} from "./IComposable"
import "./Nugget.css"; import "./Nugget.css";
import { Nugget as NuggetType, decreaseNuggetScore, increaseNuggetScore } from '../lib/prompt'; import { Nugget as NuggetType, decreaseNuggetScore, increaseNuggetScore } from '../lib/prompt';
@ -32,5 +31,5 @@ export default function Nugget(props : NuggetProps) {
</ButtonGroup> </ButtonGroup>
</span> </span>
</div> </div>
) as Composable; );
} }

View File

@ -2,8 +2,7 @@ import { Menu, MenuItem } from "@material-ui/core";
import React, { Children, ReactNode } from 'react'; import React, { Children, ReactNode } from 'react';
import "./Operation.css"; import "./Operation.css";
import { Op } from "../lib/operator"; import { Op } from "../lib/operator";
import { randomUUID } from "crypto"; import { v4 as randomUUID } from "uuid";
import { Composable } from "./IComposable";
import { Operation as OperationType, changeOperationOp } from "../lib/prompt"; import { Operation as OperationType, changeOperationOp } from "../lib/prompt";
import Nugget from "./Nugget"; import Nugget from "./Nugget";
@ -72,7 +71,7 @@ function Operation(props : OperationProps) {
})} })}
</Menu> </Menu>
</div> </div>
) as Composable; );
} }
export { Operation, Op }; export { Operation, Op };

View File

@ -1,27 +1,29 @@
import { Button } from '@material-ui/core'; import { Button } from '@material-ui/core';
import Masonry from '@mui/lab/Masonry'; import Masonry from '@mui/lab/Masonry';
import Nugget from './Nugget';
import { Operation } from './Operation';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import "./PromptArea.css"; import "./PromptComposer.css";
import { PromptLibrary } from './PromptLibrary'; import { PromptLibrary } from './PromptLibrary';
import React from 'react'; import React, { useState } from 'react';
import { $composition, $slottedComposition, LibraryItem, insertIntoComposition } from '../lib/prompt'; import { $slottedComposition, LibraryItem, PromptItem, insertIntoComposition } from '../lib/prompt';
import { Category } from '@mui/icons-material'; import { Category } from '@mui/icons-material';
import { useStore } from '@nanostores/react' 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 = () => { const handleClickOpen = () => {
setOpen(true); setOpen(true);
}; };
const handleClose = (value: string) => { const handleClose = () => {
setOpen(false); setOpen(false);
// setSelectedValue(value);
}; };
const handleOnInsertItem = (item: LibraryItem) => { const handleOnInsertItem = (item: LibraryItem) => {
@ -30,19 +32,31 @@ export default function PromptComposer(props) {
const slottedComposition = useStore($slottedComposition); const slottedComposition = useStore($slottedComposition);
const promptItemFactory = (promptItem : PromptItem, key : string) => {
return "op" in promptItem ? <Operation operation={promptItem} key={key} /> : <Nugget nugget={promptItem} key={key} />
}
return ( return (
<div> <div>
<Button className="add-button"> <div>
<AddIcon /> <Button className="add-button" onClick={handleClickOpen}>
<PromptLibrary <AddIcon />
open={open} <PromptLibrary
onInsertItem={handleOnInsertItem} open={open}
></PromptLibrary> onInsertItem={handleOnInsertItem}
onClose={handleClose}
</Button> ></PromptLibrary>
<Masonry columns={Object.keys(Category).length} spacing={2} sequential> </Button>
<div>Something</div> </div>
</Masonry> <div>
{
slottedComposition.map((itemCol, i) => (
<Stack>
{itemCol.map((promptItem, j) => promptItemFactory(promptItem, `item-${j}-${j}`))}
</Stack>
))
}
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,3 @@
.prompt-library-dialog .categories div {
display: inline;
}

View File

@ -31,6 +31,7 @@ const mockOpen: boolean = true;
const mockProps: SimpleDialogProps = { const mockProps: SimpleDialogProps = {
open: mockOpen, open: mockOpen,
onInsertItem: mockOnAddItem, onInsertItem: mockOnAddItem,
onClose: mockOnClose,
}; };
const mockLibrary: LibraryType = [ const mockLibrary: LibraryType = [

View File

@ -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 as LibItemType, $library, Category } from "../lib/prompt";
import { LibraryItem } from "./LibraryItem"; import { LibraryItem } from "./LibraryItem";
import { ChangeEvent } from "react"; import { ChangeEvent, useState } from "react";
import { useStore } from "@nanostores/react"; import { useStore } from "@nanostores/react";
import { ExitToAppOutlined } from "@mui/icons-material";
import { NewLibraryItem } from "./NewLibraryItem";
import "./PromptLibrary.css"
export interface SimpleDialogProps { export interface SimpleDialogProps {
open: boolean; open: boolean;
// onClose: (composable: Composable) => void, onClose: () => void,
// onAddItem: (item: LibItemType) => void, // onAddItem: (item: LibItemType) => void,
onInsertItem: (item: LibItemType) => void, onInsertItem: (item: LibItemType) => void,
} }
@ -25,10 +28,14 @@ const show = ($el: Element) => {
} }
export function PromptLibrary(props: SimpleDialogProps) { export function PromptLibrary(props: SimpleDialogProps) {
const { open, onInsertItem } = props; const { open, onInsertItem, onClose } = props;
const library = useStore($library); const library = useStore($library);
const [doCreate, setDoCreate] = useState(false);
const [visibleCategories, setVisibleCategories] = useState([] as Category[]);
const handleOnAddItem = (item: LibItemType) => { const handleOnAddItem = (item: LibItemType) => {
// onAddItem(item); // onAddItem(item);
} }
@ -37,32 +44,47 @@ export function PromptLibrary(props: SimpleDialogProps) {
onInsertItem(item); onInsertItem(item);
} }
const filterCat = (catKey: string, catVal: string, v: ChangeEvent<HTMLInputElement>) => { const filterCat = (catKey: string, v: ChangeEvent<HTMLInputElement>) => {
const isChecked = v.target.value === '1'; 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) if (isChecked) show($el)
else hide($el) else hide($el)
}); });
} }
const handleClose = () => {
onClose();
}
const handleOnNewCreated = () => {
setDoCreate(false);
}
const categoryChoices = Object.keys(Category);
return ( return (
<Dialog open={open}> <Dialog className="prompt-library-dialog" onClose={handleClose} open={open}>
<DialogTitle>Prompt Library</DialogTitle> <DialogTitle>Prompt Library</DialogTitle>
<div className="categories"> <div className="categories">
{Object.entries(Category).map(([catKey, catVal]) => { {categoryChoices.map(catKey => {
return ( return (
<div> <div key={catKey}>
<Checkbox onChange={v => filterCat(catKey, catVal, v)} /> <Checkbox onChange={v => filterCat(catKey, v)} />
<span>{title(catVal)}</span> <span>{title(catKey)}</span>
</div> </div>
) )
})} })}
</div> </div>
<div> <div>
{ {
library?.map(item => <LibraryItem item={item} onInsertItem={handleOnInsertItem} />) library?.map(item => <LibraryItem key={item.id} item={item} onInsertItem={handleOnInsertItem} />)
} }
</div> </div>
<div>
<Button onClick={() => setDoCreate(true)}>Create</Button>
</div>
{doCreate ? (<NewLibraryItem onNewCreated={handleOnNewCreated} />) : (<></>)}
</Dialog> </Dialog>
); );
} }

View File

@ -0,0 +1,6 @@
.text-prompt {
width: 75%;
height: 75%;
padding: 10pt;
margin: 4pt;
}

View File

@ -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 (
<TextareaAutosize content={text} className="text-prompt" />
)
}

View File

@ -3,17 +3,13 @@ import ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from "./store"
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
); );
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <App />
<App />
</Provider >
</React.StrictMode> </React.StrictMode>
); );

View File

@ -1,4 +1,4 @@
import { randomUUID } from "crypto"; import { v4 as randomUUID } from "uuid";
import { Op } from "./operator"; import { Op } from "./operator";
import { createSlice } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit'

View File

@ -1,4 +1,4 @@
import { randomUUID } from "crypto"; import { v4 as randomUUID } from "uuid";
import { Op } from "./operator"; import { Op } from "./operator";
import { import {
Library as LibraryType, Library as LibraryType,

View File

@ -1,4 +1,4 @@
import { randomUUID } from "crypto"; import { v4 as randomUUID } from "uuid";
import { Op } from "./operator"; import { Op } from "./operator";
import { atom, computed } from "nanostores"; import { atom, computed } from "nanostores";
@ -8,6 +8,7 @@ type IdAble = {
id: Id, id: Id,
} }
// only vibes and styles will have a name.
export enum Category { export enum Category {
subject = "subject", subject = "subject",
style = "style", style = "style",
@ -15,6 +16,10 @@ export enum Category {
medium = "medium", medium = "medium",
} }
export function categoryHasName(cat : Category) {
return (cat === Category.style || cat === Category.vibes)
}
const N_CATEGORIES = Object.keys(Category).length; const N_CATEGORIES = Object.keys(Category).length;
export function catI(c: Category | string): number { export function catI(c: Category | string): number {