Skip to content

Commit

Permalink
Merge pull request #135 from yello-xyz/v0.22
Browse files Browse the repository at this point in the history
V0.22
  • Loading branch information
hverlind authored Jan 12, 2024
2 parents 8c90521 + d28547a commit 596a0a3
Show file tree
Hide file tree
Showing 71 changed files with 1,771 additions and 1,124 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## PlayFetch Changelog

### v0.22 - 2024-01-12
- Improved layout for prompts
- Support for Gemini Pro (preview)

### v0.21 - 2024-01-05
- Improved text editor performance and usability
- Show token counts in responses
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ Or just **Open with GitHub Desktop** and clone to a local directory (recommended
## Configure environment
`cd ~/[LOCAL_DIRECTORY]/playfetch`

In order to access the Google Cloud Datastore from your local machine, you will need to install the Google Cloud CLI initialize it as explained [here](https://cloud.google.com/sdk/docs/install-sdk) (you can skip the other steps). Run the following commands to log in with your individual Google account (which should be added to the [email protected] group).

`gcloud auth login`

`gcloud init`

In order to run the app locally, you will need to add some additional variables to your local `.env.local` file (this file is ignored by source control to avoid leaking keys). For most of these you should avoid using the same values as used in the production environment (e.g. generate your own API free keys so you don't risk messing up analytics or rate limits while testing locally):

`API_URL=http://localhost:3000`
Expand Down Expand Up @@ -95,12 +101,6 @@ In order to run the app locally, you will need to add some additional variables

`NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=GTM-NCN8W45M`

In order to access the Google Cloud Datastore from your local machine, you will need to install the Google Cloud CLI initialize it as explained [here](https://cloud.google.com/sdk/docs/install-sdk) (you can skip the other steps). Run the following commands to log in with your individual Google account (which should be added to the [email protected] group).

`gcloud auth login`

`gcloud init`

## Build and run
`npm install`

Expand Down
5 changes: 5 additions & 0 deletions __tests__/sanitizePopupLocation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ const testPopupLocation = (testDescription: string, location: GlobalPopupLocatio

testPopupLocation('Unspecified', {}, { left: 200, top: 150 })
testPopupLocation('Fully specified', { left: 100, top: 100, right: 100, bottom: 100 })
testPopupLocation(
'Fully specified with zero top and bottom centered vertically',
{ top: 0, left: 100, right: 100, bottom: 0 },
{ top: 150, left: 100, right: 100, bottom: undefined }
)
testPopupLocation('Top left', { left: 100, top: 100 }, { left: 100, top: 100 })
testPopupLocation('Bottom right', { right: 700, bottom: 500 }, { right: 100, bottom: 100 })
testPopupLocation('Adjusts top left down', { left: 600, top: 400 }, { left: 400, top: 300 })
Expand Down
80 changes: 24 additions & 56 deletions __tests__/selectInputRows.test.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,61 @@
import { SelectInputRows } from '@/src/client/inputRows'
import { InputValues, TestConfig } from '@/types'

const config = (mode: TestConfig['mode'], rowIndices = [] as number[]): TestConfig => ({ mode, rowIndices })

test('No inputs yields single empty prompt input', () =>
expect(SelectInputRows({}, [], config('first'))[0]).toStrictEqual([{}]))
expect(SelectInputRows({}, [], { rowIndices: [] })[0]).toStrictEqual([{}]))

test('Inputs without values yield single empty prompt input', () =>
expect(SelectInputRows({}, ['a', 'b'], config('first'))[0]).toStrictEqual([{}]))
expect(SelectInputRows({}, ['a', 'b'], { rowIndices: [] })[0]).toStrictEqual([{}]))

const testRowSelection = (
testDescription: string,
inputValues: InputValues,
variables: string[],
config: TestConfig,
count: number,
start: number,
config: TestConfig | undefined,
expectedIndices: number[]
) =>
test(testDescription, () =>
expect(SelectInputRows(inputValues, variables, config, count, start)[1]).toStrictEqual(expectedIndices)
)
test(testDescription, () => expect(SelectInputRows(inputValues, variables, config)[1]).toStrictEqual(expectedIndices))

const testAllDefaultModes = (
const testDefaultSelection = (
testDescription: string,
inputValues: InputValues,
variables: string[],
expectedIndices: number[]
) => {
for (const mode of ['first', 'last', 'random', 'all'] as TestConfig['mode'][]) {
testRowSelection(
`${testDescription} (${mode})`,
inputValues,
variables,
config(mode),
expectedIndices.length,
0,
expectedIndices
)
}
testRowSelection(testDescription, inputValues, variables, { rowIndices: [] }, expectedIndices)
testRowSelection(`${testDescription} (all)`, inputValues, variables, undefined, expectedIndices)
}

testAllDefaultModes('Test empty input', {}, [], [])
testAllDefaultModes('Test empty input with variables', {}, ['a', 'b', 'c'], [])
testAllDefaultModes('Test single input without values', { var1: [] }, ['var1'], [])
testAllDefaultModes('Test single input without variables', { var1: ['value1'] }, [], [])
testAllDefaultModes('Test single input with different variables', { var1: ['value1'] }, ['var2'], [])
testAllDefaultModes('Test single input with single variable', { var1: ['value1'] }, ['var1'], [0])
testAllDefaultModes('Test single input with additional variables', { var1: ['value1'] }, ['var1', 'var2'], [0])
testDefaultSelection('Test empty input', {}, [], [])
testDefaultSelection('Test empty input with variables', {}, ['a', 'b', 'c'], [])
testDefaultSelection('Test single input without values', { var1: [] }, ['var1'], [])
testDefaultSelection('Test single input without variables', { var1: ['value1'] }, [], [])
testDefaultSelection('Test single input with different variables', { var1: ['value1'] }, ['var2'], [])
testDefaultSelection('Test single input with single variable', { var1: ['value1'] }, ['var1'], [0])
testDefaultSelection('Test single input with additional variables', { var1: ['value1'] }, ['var1', 'var2'], [0])

const sparseValues = {
var1: ['', 'value1', '', '', '', '', ''],
var2: ['', '', '', 'value2', '', '', ''],
var3: ['', '', '', '', '', 'value3', ''],
}

const testSparseValues = (config: TestConfig, expectedIndices: number[], count = 1, start = 0) => {
const testSparseValues = (config: TestConfig | undefined, expectedIndices: number[], count = 1, start = 0) => {
testRowSelection(
`Test sparse values (${config.mode} [${config.rowIndices}]) [${count}, ${start}]`,
`Test sparse values (${config ? 'custom' : 'all'} [${config?.rowIndices ?? []}]) [${count}, ${start}]`,
sparseValues,
Object.keys(sparseValues),
config,
count,
start,
expectedIndices
)
}

testSparseValues(config('first'), [1])
testSparseValues(config('last'), [5])
testSparseValues(config('all'), [1, 3, 5])

testSparseValues(config('custom'), [])
testSparseValues(config('custom', [0]), [])
testSparseValues(config('custom', [1, 2]), [1])
testSparseValues(config('custom', [1, 2, 3]), [1, 3])
testSparseValues(config('custom', [3, 2, 1]), [1, 3])
testSparseValues(config('custom', [1, 3, 5]), [1, 3, 5])
testSparseValues(config('custom', [0, 1, 2, 3, 4, 5, 6, 7, 8]), [1, 3, 5])
testSparseValues({ rowIndices: [] }, [1])
testSparseValues(undefined, [1, 3, 5])

testSparseValues(config('range'), [1])
testSparseValues(config('range'), [1], 1, 1)
testSparseValues(config('range'), [3], 1, 2)
testSparseValues(config('range'), [1, 3], 2, 0)
testSparseValues(config('range'), [3, 5], 2, 2)
testSparseValues(config('range'), [3, 5], 2, 3)
testSparseValues(config('range'), [1, 3, 5], 3, 0)
testSparseValues(config('range'), [1, 3, 5], 3, 1)
testSparseValues(config('range'), [3, 5], 3, 2)
testSparseValues(config('range'), [5], 1, 5)
testSparseValues(config('range'), [5], 2, 5)
testSparseValues(config('range'), [1, 3, 5], 10, 0)
testSparseValues({ rowIndices: [0] }, [1])
testSparseValues({ rowIndices: [1, 2] }, [1])
testSparseValues({ rowIndices: [1, 2, 3] }, [1, 3])
testSparseValues({ rowIndices: [3, 2, 1] }, [1, 3])
testSparseValues({ rowIndices: [1, 3, 5] }, [1, 3, 5])
testSparseValues({ rowIndices: [0, 1, 2, 3, 4, 5, 6, 7, 8] }, [1, 3, 5])
4 changes: 2 additions & 2 deletions components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default function Button({
case 'secondary':
return 'text-gray-700 bg-white border border-gray-200 hover:bg-gray-50 hover:border-gray-300 disabled:opacity-50'
case 'outline':
return 'bg-white border border-gray-200 hover:bg-gray-100 font-medium disabled:opacity-50'
return 'bg-white border border-gray-200 hover:bg-gray-100 hover:border-gray-300 font-medium disabled:opacity-50'
case 'destructive':
return 'bg-white text-red-500 border border-gray-200 hover:bg-red-50 hover:border-red-100 disabled:opacity-50'
}
Expand All @@ -81,7 +81,7 @@ export default function Button({
<button
className={`${colorForType(
type
)} ${paddingClass} ${roundedClass} h-9 text-sm whitespace-nowrap flex items-center`}
)} ${paddingClass} ${roundedClass} h-8 text-sm whitespace-nowrap flex items-center`}
disabled={disabled}
onClick={onClick}>
{showSpinner && <Icon icon={spinnerIcon} className='animate-spin max-w-[24px]' />}
Expand Down
4 changes: 2 additions & 2 deletions components/chains/chainEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function ChainEditor({
activeIndex: number | undefined
setActiveIndex: (index: number) => void
prompts: Prompt[]
addPrompt: () => Promise<{ promptID: number; versionID: number }>
addPrompt: () => Promise<number>
showVersions: boolean
setShowVersions?: (show: boolean) => void
isTestMode: boolean
Expand Down Expand Up @@ -93,7 +93,7 @@ export default function ChainEditor({
})

const insertNewPrompt = (index: number, branch: number) =>
addPrompt().then(({ promptID, versionID }) => insertPrompt(index, branch, promptID, versionID))
addPrompt().then(promptID => insertPrompt(index, branch, promptID))

const insertCodeBlock = (index: number, branch: number) => insertItem(index, branch, { code: '' })
const insertBranch = (index: number, branch: number) =>
Expand Down
26 changes: 11 additions & 15 deletions components/chains/chainNodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ export default function ChainNodeEditor({
const isPromptChainItemActive = IsPromptChainItem(activeItem)
const activePrompt = isPromptChainItemActive ? promptCache.promptForItem(activeItem) : undefined
const initialActivePromptVersion = isPromptChainItemActive ? promptCache.versionForItem(activeItem) : undefined
const [activePromptVersion, setActivePromptVersion] = useState(initialActivePromptVersion)
const [activePromptVersion, setActivePromptVersion] = useInitialState(
initialActivePromptVersion,
(a, b) => a?.id === b?.id
)
const [savePrompt, setModifiedVersion] = useSavePrompt(activePrompt, activePromptVersion, setActivePromptVersion)

const updateVersion = (version?: PromptVersion) => {
Expand All @@ -69,27 +72,22 @@ export default function ChainNodeEditor({
}

const selectVersion = (version?: PromptVersion) => {
saveAndRefreshPrompt()
isPromptChainItemActive && savePrompt(() => promptCache.refreshItem(activeItem.promptID))
setActivePromptVersion(version)
updatePromptDirty(false)
if (version) {
updateActiveItem({ ...activeItem, versionID: version.id })
}
}

const saveAndRefreshPrompt = (onSavePrompt?: (versionID: number) => void) => {
if (isPromptChainItemActive) {
return savePrompt(() => promptCache.refreshPrompt(activeItem.promptID)).then(
versionID => versionID && onSavePrompt?.(versionID)
)
}
}

const setDialogPrompt = useModalDialogPrompt()

const saveAndClose = async () => {
saveItems(updatedItems)
await saveAndRefreshPrompt(versionID => saveItems(itemsWithUpdate({ ...activeItem, versionID })))
if (isPromptChainItemActive) {
savePrompt().then(versionID => versionID && saveItems(itemsWithUpdate({ ...activeItem, versionID })))
} else {
saveItems(updatedItems)
}
dismiss()
}

Expand All @@ -107,11 +105,9 @@ export default function ChainNodeEditor({
}
}

const colorClass = IsPromptChainItem(activeItem) ? 'bg-white' : 'bg-gray-25'

return (
<>
<div className={`flex flex-col items-end flex-1 h-full gap-4 pb-4 overflow-hidden ${colorClass}`}>
<div className='flex flex-col items-end flex-1 h-full gap-4 pb-4 overflow-hidden'>
{IsPromptChainItem(activeItem) && (
<PromptNodeEditor
item={activeItem}
Expand Down
6 changes: 3 additions & 3 deletions components/chains/chainNodeOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default function ChainNodeOutput({
const activeNode = nodes[activeIndex]
const items = nodes.filter(IsChainItem)
const [inputValues, setInputValues, persistInputValuesIfNeeded] = useInputValues(chain, JSON.stringify(activeNode))
const [testConfig, setTestConfig] = useState<TestConfig>({ mode: 'first', rowIndices: [0] })
const [testConfig, setTestConfig] = useState<TestConfig>({ rowIndices: [0] })

const [checkProviderAvailable, checkModelAvailable] = useCheckProviders()
const areProvidersAvailable = (items: ChainItem[]) =>
Expand Down Expand Up @@ -199,7 +199,7 @@ export default function ChainNodeOutput({
<div className='flex flex-col items-end flex-1 h-full gap-4 pb-4 overflow-hidden'>
{activeNode === InputNode && canShowTestData ? (
<div className='flex flex-col flex-1 w-full overflow-y-auto'>
<SingleTabHeader label='Test data' />
<SingleTabHeader label='Test Data' />
<TestDataPane
variables={variables}
staticVariables={staticVariables}
Expand Down Expand Up @@ -230,7 +230,7 @@ export default function ChainNodeOutput({
</div>
)}
{showRunButtons && (
<div className='flex items-center justify-end w-full gap-4 px-4'>
<div className='flex items-center w-full gap-4 px-4'>
<RunButtons
variables={variables}
staticVariables={staticVariables}
Expand Down
22 changes: 15 additions & 7 deletions components/chains/chainView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ActiveChain, ChainItem, ChainItemWithInputs, ChainVersion } from '@/types'
import { useState } from 'react'
import { useCallback, useState } from 'react'
import api from '@/src/client/api'
import ChainNodeEditor from './chainNodeEditor'
import ChainEditor from './chainEditor'
Expand Down Expand Up @@ -77,11 +77,21 @@ export default function ChainView({
const [savedItemsKey, setSavedItemsKey] = useState(itemsKey)

const refreshActiveItem = useRefreshActiveItem()
const refreshProject = useRefreshProject()

const refreshOnSave = useCallback(async () => {
await refreshActiveItem()
const activeNode = activeNodeIndex !== undefined ? nodes[activeNodeIndex] : undefined
if (activeNode && IsPromptChainItem(activeNode)) {
await refreshProject()
promptCache.refreshItem(activeNode.promptID)
}
}, [nodes, activeNodeIndex, promptCache, refreshActiveItem, refreshProject])

const saveItems = (items: ChainItem[]): Promise<number | undefined> => {
const loadedItems = items.filter(item => !IsPromptChainItem(item) || !!item.versionID)
setSavedItemsKey(GetChainItemsSaveKey(loadedItems))
return saveChain(GetItemsToSave(loadedItems, promptCache), refreshActiveItem)
return saveChain(GetItemsToSave(loadedItems, promptCache), refreshOnSave)
}

const [syncedVersionID, setSyncedVersionID] = useState(activeVersion.id)
Expand All @@ -95,12 +105,10 @@ export default function ChainView({
setSavedItemsKey(GetChainItemsSaveKey(activeVersion.items))
}

const refreshProject = useRefreshProject()

const addPrompt = async () => {
const { promptID, versionID } = await api.addPrompt(activeProject.id)
const promptID = await api.addPrompt(activeProject.id)
refreshProject()
return { promptID, versionID }
return promptID
}

const isInputOutputIndex = (index: number | undefined, nodes: ChainNode[]) =>
Expand Down Expand Up @@ -200,7 +208,7 @@ export default function ChainView({
activeNodeIndex !== undefined &&
(isTestMode || !isInputOutputNode) &&
!isUnloadedPromptNode(nodes[activeNodeIndex]) && (
<Allotment.Pane minSize={minWidth}>
<Allotment.Pane className='bg-gray-25' minSize={minWidth}>
{isTestMode ? (
<ChainNodeOutput
chain={chain}
Expand Down
Loading

0 comments on commit 596a0a3

Please sign in to comment.