Skip to content

Commit

Permalink
Merge pull request #86 from yello-xyz/v0.12
Browse files Browse the repository at this point in the history
V0.12
  • Loading branch information
hverlind authored Oct 20, 2023
2 parents 1aee972 + 75ef7eb commit 62f48c7
Show file tree
Hide file tree
Showing 109 changed files with 2,345 additions and 884 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## PlayFetch Changelog

### v0.12 - 2023-10-20
- Admin dashboards for active users and projects
- Project and workspace invites with notifications
- Implicit saving for chains

### v0.11 - 2023-10-13
- Improved test row selector
- Text-based diffing in Compare tool for prompts, chains and endpoints
Expand Down
2 changes: 1 addition & 1 deletion __tests__/consumePartialRuns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const testConsumeStream = (
numberOfInputs = 1
) =>
test(`Test ${testDescription}`, async () => {
const inputs = Array.from({ length: numberOfInputs }, () => ({} as PromptInputs))
const inputs = Array.from({ length: numberOfInputs }, () => ({}) as PromptInputs)
const streamReader = new ReadableStream({
start(controller) {
for (const data of streamedData) {
Expand Down
65 changes: 65 additions & 0 deletions __tests__/extractVariables.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ExtractPromptVariables, ExtractVariables } from '@/src/common/formatting'
import { ChainItemWithInputs, PromptConfig, Prompts } from '@/types'
import { DefaultConfig } from '@/src/common/defaultConfig'
import { ExtractUnboundChainInputs } from '@/components/chains/chainNodeOutput'

const testExtractVariables = (testDescription: string, expectedVariables: string[], content: string) =>
test(`Test ${testDescription}`, () => {
expect(ExtractVariables(content)).toStrictEqual(expectedVariables)
})

testExtractVariables('empty content', [], '')
testExtractVariables('no variables', [], 'hello world')
testExtractVariables('non variable', [], '}}hello{{')
testExtractVariables('single variable', ['hello'], '{{hello}}')
testExtractVariables('single variable start', ['hello'], '{{hello}} world')
testExtractVariables('single variable end', ['world'], 'hello {{world}}')

const configWithFunctionsSupport: PromptConfig = { ...DefaultConfig, model: 'gpt-4' }

const buildPrompt = (main: string, system?: string, functions?: string): Prompts => ({ main, system, functions })

const testExtractPromptVariables = (
testDescription: string,
expectedVariables: string[],
prompts: Prompts,
includingDynamic = true
) =>
test(`Test ${testDescription}`, () => {
expect(ExtractPromptVariables(prompts, configWithFunctionsSupport, includingDynamic)).toStrictEqual(
expectedVariables
)
})

testExtractPromptVariables('empty prompts', [], buildPrompt(''))
testExtractPromptVariables('main prompt', ['hello'], buildPrompt('{{hello}}'))
testExtractPromptVariables('system prompt', ['hello', 'world'], buildPrompt('{{hello}}', '{{world}}'))
testExtractPromptVariables('functions prompt', ['hello', 'world'], buildPrompt('{{hello}}', '', '{{world}}'))
testExtractPromptVariables('include dynamic', ['hello_world'], buildPrompt('', '', '[{ "name": "hello_world" }]'))
testExtractPromptVariables('exclude dynamic', [], buildPrompt('', '', '[{ "name": "hello_world" }]'), false)

const buildChain = (inputs: string[], outputs: string[]): ChainItemWithInputs[] =>
inputs.map((input, index) => ({
code: '',
inputs: input ? [input] : [],
output: outputs[index],
}))

const testExtractChainVariables = (
testDescription: string,
expectedVariables: string[],
inputs: string[],
outputs: string[] = []
) =>
test(`Test ${testDescription}`, () => {
expect(ExtractUnboundChainInputs(buildChain(inputs, outputs), false)).toStrictEqual(expectedVariables)
})

testExtractChainVariables('empty chain', [], [], [])
testExtractChainVariables('single step single chain input', ['hello'], ['hello'])
testExtractChainVariables('multi step single chain input', ['hello'], ['', 'hello'])
testExtractChainVariables('multi step repeated chain input', ['hello'], ['hello', '', 'hello'])
testExtractChainVariables('multi step multi chain input', ['hello', 'world'], ['hello', '', 'world'])
testExtractChainVariables('mapped input', ['world'], ['', 'hello', 'world'], ['hello'])
testExtractChainVariables('mapped too late', ['hello'], ['hello', ''], ['', 'hello'])
testExtractChainVariables('mapped later too', ['hello', 'world'], ['hello', 'world', 'hello'], ['', 'hello'])
2 changes: 1 addition & 1 deletion app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ env: flex

runtime_config:
operating_system: "ubuntu22"
runtime_version: 18
runtime_version: "18"
13 changes: 8 additions & 5 deletions cloudbuild.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
steps:
- name: node
- name: node:18
entrypoint: node
args: ['--version']
- name: node:18
entrypoint: npm
args: ['install']
- name: node
- name: node:18
entrypoint: npm
args: ['run', 'test']
- name: node
- name: node:18
entrypoint: npm
args: ['run', 'clean']
- name: node
- name: node:18
entrypoint: npm
args: ['run', 'create-env']
env:
Expand All @@ -33,7 +36,7 @@ steps:
- 'NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=${_NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID}'
- 'NEXT_PUBLIC_COOKIE_DOMAIN=${_NEXT_PUBLIC_COOKIE_DOMAIN}'
- 'NEXT_PUBLIC_COOKIE_NAME=${_NEXT_PUBLIC_COOKIE_NAME}'
- name: node
- name: node:18
entrypoint: npm
args: ['run', 'build']
- name: 'gcr.io/cloud-builders/gcloud'
Expand Down
58 changes: 58 additions & 0 deletions components/admin/activeUserMetrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ActiveUser, UserMetrics } from '@/types'
import Label from '@/components/label'
import UserAvatar from '@/components/users/userAvatar'
import { FormatCost, FormatDate } from '@/src/common/formatting'
import Icon from '../icon'
import backIcon from '@/public/back.svg'
import { LabelForProvider } from '@/src/common/providerMetadata'

export default function ActiveUserMetrics({
user,
metrics,
onDismiss,
}: {
user: ActiveUser
metrics: UserMetrics
onDismiss: () => void
}) {
return (
<>
<div className='flex flex-col items-start h-full gap-4 p-6 overflow-y-auto'>
<Label onClick={onDismiss} className='flex items-center cursor-pointer'>
<Icon icon={backIcon} />
Back to Active Users
</Label>
<div className='flex flex-col gap-4 p-4 bg-white border border-gray-200 rounded-lg'>
<div className='flex items-center gap-2'>
<UserAvatar user={user} />
<Label>
{user.fullName} ({user.email})
</Label>
</div>
<Label>Last Active: {FormatDate(user.lastActive, true, true)}</Label>
<div className='flex flex-col gap-1'>
<Label>Number of additional workspaces created: {metrics.createdWorkspaceCount - 1}</Label>
<Label>
Number of additional workspaces shared with user:{' '}
{metrics.workspaceAccessCount - metrics.createdWorkspaceCount}
</Label>
<Label>Number of additional projects shared with user: {metrics.projectAccessCount}</Label>
</div>
<div className='flex flex-col gap-1'>
<Label>Total number of versions created: {metrics.createdVersionCount}</Label>
<Label>Total number of comments made: {metrics.createdCommentCount}</Label>
<Label>Total number of endpoints published: {metrics.createdEndpointCount}</Label>
</div>
<Label>Registered Providers:</Label>
<div className='flex flex-col gap-1'>
{metrics.providers.map((provider, index) => (
<Label key={index}>
{LabelForProvider(provider.provider)} ({FormatCost(provider.cost)})
</Label>
))}
</div>
</div>
</div>
</>
)
}
84 changes: 84 additions & 0 deletions components/admin/activeUsers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { ReactNode } from 'react'
import { ActiveUser } from '@/types'
import Label from '@/components/label'
import UserAvatar from '@/components/users/userAvatar'
import { FormatDate } from '@/src/common/formatting'

export default function ActiveUsers({
title = 'Active Users',
activeUsers,
onSelectUser,
embedded,
}: {
title?: string
activeUsers: ActiveUser[]
onSelectUser: (userID: number) => void
embedded?: boolean
}) {
const gridConfig = 'grid grid-cols-[100px_200px_minmax(0,1fr)_100px_100px_100px_100px_100px]'

const startDate = Math.min(...activeUsers.map(user => user.startTimestamp))

return (
<>
<div className={`flex flex-col items-start gap-4 ${embedded ? '' : 'p-6 overflow-y-auto'}`}>
{activeUsers.length > 0 && (
<>
<Label>
{title} (data since {FormatDate(startDate)})
</Label>
<div className={`${gridConfig} bg-white items-center border-gray-200 border rounded-lg p-2`}>
<TableCell>
<Label>Last Active</Label>
</TableCell>
<TableCell>
<Label>Name</Label>
</TableCell>
<TableCell>
<Label>Email</Label>
</TableCell>
<TableCell center>
<Label># Comments</Label>
</TableCell>
<TableCell center>
<Label># Versions</Label>
</TableCell>
<TableCell center>
<Label># Prompts</Label>
</TableCell>
<TableCell center>
<Label># Chains</Label>
</TableCell>
<TableCell center>
<Label># Endpoints</Label>
</TableCell>
{activeUsers.map(user => (
<div key={user.id} className='cursor-pointer contents group' onClick={() => onSelectUser(user.id)}>
<TableCell>{FormatDate(user.lastActive, false)}</TableCell>
<TableCell>
<UserAvatar user={user} /> {user.fullName}
</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell center>{user.commentCount}</TableCell>
<TableCell center>{user.versionCount}</TableCell>
<TableCell center>{user.promptCount}</TableCell>
<TableCell center>{user.chainCount}</TableCell>
<TableCell center>{user.endpointCount}</TableCell>
</div>
))}
</div>
</>
)}
</div>
</>
)
}

const TableCell = ({ children, center }: { children: ReactNode; center?: boolean }) => (
<div
className={`flex items-center gap-2 px-2 h-10 overflow-hidden font-medium text-ellipsis group-hover:bg-gray-50 ${
center ? 'justify-center' : ''
}`}>
{children}
</div>
)
37 changes: 37 additions & 0 deletions components/admin/adminSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { AdminRoute } from '@/src/common/clientRoute'
import { SidebarButton, SidebarSection } from '../sidebar'
import linkIcon from '@/public/chain.svg'
import userIcon from '@/public/user.svg'
import fileIcon from '@/public/file.svg'

export default function AdminSidebar({
onSelectWaitlist,
onSelectActiveUsers,
onSelectRecentProjects,
}: {
onSelectWaitlist: () => void
onSelectActiveUsers: () => void
onSelectRecentProjects: () => void
}) {
return (
<>
<div className='flex flex-col gap-4 px-2 pt-3 pb-4 border-r border-gray-200 bg-gray-25'>
<SidebarSection title='Manage Access'>
<SidebarButton title='Waitlist' icon={userIcon} onClick={onSelectWaitlist} />
</SidebarSection>
<SidebarSection title='Recent Activity'>
<SidebarButton title='Active Users' icon={userIcon} onClick={onSelectActiveUsers} />
<SidebarButton title='Active Projects' icon={fileIcon} onClick={onSelectRecentProjects} />
</SidebarSection>
<SidebarSection title='Google Analytics'>
<SidebarButton title='Dashboards' icon={linkIcon} link={AdminRoute.AnalyticsDashboard} target='_blank' />
<SidebarButton title='Reports' icon={linkIcon} link={AdminRoute.AnalyticsReports} target='_blank' />
<SidebarButton title='Search Console' icon={linkIcon} link={AdminRoute.SearchConsole} target='_blank' />
</SidebarSection>
<SidebarSection title='Debug'>
<SidebarButton title='Server Logs' icon={linkIcon} link={AdminRoute.ServerLogs} target='_blank' />
</SidebarSection>
</div>
</>
)
}
60 changes: 60 additions & 0 deletions components/admin/recentProjectMetrics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ProjectMetrics, RecentProject } from '@/types'
import Label from '@/components/label'
import Icon from '../icon'
import backIcon from '@/public/back.svg'
import AnalyticsDashboards from '../endpoints/analyticsDashboards'
import { FormatDate } from '@/src/common/formatting'
import ActiveUsers from './activeUsers'
import fileIcon from '@/public/file.svg'
import folderIcon from '@/public/folder.svg'

export default function RecentProjectMetrics({
project,
metrics,
onSelectUser,
onDismiss,
}: {
project: RecentProject
metrics: ProjectMetrics
onSelectUser: (userID: number) => void
onDismiss: () => void
}) {
return (
<>
<div className='flex flex-col items-start h-full gap-4 p-6 overflow-y-auto'>
<Label onClick={onDismiss} className='flex items-center cursor-pointer'>
<Icon icon={backIcon} />
Back to Active Projects
</Label>
<div className='flex flex-col gap-4 p-4 bg-white border border-gray-200 rounded-lg'>
<div className='flex items-center'>
<Icon icon={fileIcon} />
<Label>{project.name}</Label>
<Icon icon={folderIcon} />
<Label>
{project.workspace} ({project.creator})
</Label>
</div>
<Label>Last Modified: {FormatDate(project.timestamp, true, true)}</Label>
<div className='flex flex-col gap-1'>
<Label>Number of prompts: {metrics.promptCount}</Label>
<Label>Number of chains: {metrics.chainCount}</Label>
<Label>Number of endpoints: {metrics.endpointCount}</Label>
</div>
</div>
<div className='w-full '>
<AnalyticsDashboards analytics={metrics.analytics} />
</div>
<ActiveUsers title='Active Project Users' activeUsers={metrics.users} onSelectUser={onSelectUser} embedded />
{metrics.pendingUsers.length > 0 && (
<ActiveUsers
title='Pending Invitations'
activeUsers={metrics.pendingUsers}
onSelectUser={onSelectUser}
embedded
/>
)}
</div>
</>
)
}
Loading

0 comments on commit 62f48c7

Please sign in to comment.