Compare commits
139 Commits
python-sdk
...
examples
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db69b8e496 | ||
|
|
38e28fa30a | ||
|
|
b4cb931f6c | ||
|
|
40638a7848 | ||
|
|
14eae45d18 | ||
|
|
13bac46e0b | ||
|
|
12d01cd3d5 | ||
|
|
ec59252010 | ||
|
|
87e2339df2 | ||
|
|
75ad6619a5 | ||
|
|
4b8941d53a | ||
|
|
0d691d17cc | ||
|
|
815d4faad2 | ||
|
|
9632ccbc71 | ||
|
|
a4131e4a10 | ||
|
|
db1c8f171d | ||
|
|
678392ef17 | ||
|
|
af722128e8 | ||
|
|
50a79b6e3a | ||
|
|
f59150ff5b | ||
|
|
b58e0a8d54 | ||
|
|
dc82a3fa82 | ||
|
|
fedbf5784e | ||
|
|
888c04af50 | ||
|
|
1b36453051 | ||
|
|
2f37b3ed87 | ||
|
|
8fa7b691db | ||
|
|
17866a5249 | ||
|
|
947eba3216 | ||
|
|
ef1f9458f4 | ||
|
|
c6c7e746ee | ||
|
|
3be0a90960 | ||
|
|
9b1f2ac30a | ||
|
|
1b394cc72b | ||
|
|
26b9731bab | ||
|
|
7c8ec8f6a7 | ||
|
|
10dd53e7f6 | ||
|
|
b1802fc04b | ||
|
|
f2135ddc72 | ||
|
|
ca89eafb0b | ||
|
|
b50d47beaf | ||
|
|
733d53625b | ||
|
|
a5e59e4235 | ||
|
|
d0102e3202 | ||
|
|
bd571c4c4e | ||
|
|
296eb23d97 | ||
|
|
4e2ae7a441 | ||
|
|
072dcee376 | ||
|
|
94464c0617 | ||
|
|
980644f13c | ||
|
|
6a56250001 | ||
|
|
b1c7bbbd4a | ||
|
|
3e20fa31ca | ||
|
|
48a8e64be1 | ||
|
|
f3a5f11195 | ||
|
|
da5cbaf4dc | ||
|
|
acf74909c9 | ||
|
|
edac8da4a8 | ||
|
|
687f3dd85f | ||
|
|
0cef3ab5bd | ||
|
|
756b3185de | ||
|
|
3776ffc4c3 | ||
|
|
82549122e1 | ||
|
|
56a96a7db6 | ||
|
|
1596b15727 | ||
|
|
70d4a5bd9a | ||
|
|
c6ec901374 | ||
|
|
ad7665664a | ||
|
|
108e3d1e85 | ||
|
|
76f600722a | ||
|
|
d9a0e4581f | ||
|
|
b9251ad93c | ||
|
|
809ef04dc1 | ||
|
|
0fba2c9ee7 | ||
|
|
ac2ca0f617 | ||
|
|
73b9e40ced | ||
|
|
3447e863cc | ||
|
|
897e77b054 | ||
|
|
b22a4cd93b | ||
|
|
3547c85c86 | ||
|
|
9636fa033e | ||
|
|
890a738568 | ||
|
|
7003595e76 | ||
|
|
00df4453d3 | ||
|
|
4c325fc1cc | ||
|
|
dfee8a0ed7 | ||
|
|
0b4e116783 | ||
|
|
2bcb1d16a3 | ||
|
|
6e7efee21e | ||
|
|
bb9c3a9e61 | ||
|
|
11bfb5d5e4 | ||
|
|
b00ab933b3 | ||
|
|
8f4e7f7e2e | ||
|
|
634739c045 | ||
|
|
9a9cbe8fd4 | ||
|
|
649dc3376b | ||
|
|
05e774d021 | ||
|
|
0e328b13dc | ||
|
|
0a18ca9cd6 | ||
|
|
a5fe35912e | ||
|
|
3d3ddbe7a9 | ||
|
|
d8a5617dee | ||
|
|
5da62fdc29 | ||
|
|
754e273049 | ||
|
|
2863dc2f89 | ||
|
|
c4cef35717 | ||
|
|
8552baf632 | ||
|
|
f41e2229ca | ||
|
|
e649f42c9c | ||
|
|
99f305483b | ||
|
|
b28f4cad57 | ||
|
|
df4a3a0950 | ||
|
|
e423ad656a | ||
|
|
7d0d94de3a | ||
|
|
344b257db4 | ||
|
|
28b43b6e6d | ||
|
|
8d373ec9b5 | ||
|
|
537525667d | ||
|
|
519367c553 | ||
|
|
1a338ec863 | ||
|
|
01d0b8f778 | ||
|
|
d99836ec30 | ||
|
|
33751c12d2 | ||
|
|
89815e1f7f | ||
|
|
5fa5109f34 | ||
|
|
b06ab2cbf9 | ||
|
|
35fb554038 | ||
|
|
f238177277 | ||
|
|
723c0f7505 | ||
|
|
ce6936f753 | ||
|
|
2a80cbf74a | ||
|
|
098805ef25 | ||
|
|
ed90bc5a99 | ||
|
|
de9be8c7ce | ||
|
|
3e02bcf9b8 | ||
|
|
cef2ee31fb | ||
|
|
228c547839 | ||
|
|
e1fcc8fb38 | ||
|
|
3a908d51aa |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
**/node_modules/
|
||||||
|
.git
|
||||||
|
**/.venv/
|
||||||
|
**/.env*
|
||||||
|
**/.next/
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
|||||||
.env
|
.env
|
||||||
.venv/
|
.venv/
|
||||||
*.pyc
|
*.pyc
|
||||||
|
node_modules/
|
||||||
|
*.tsbuildinfo
|
||||||
|
dist/
|
||||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*.schema.json
|
||||||
|
app/pnpm-lock.yaml
|
||||||
16
README.md
16
README.md
@@ -1,10 +1,8 @@
|
|||||||
<!-- <img src="https://github.com/openpipe/openpipe/assets/41524992/ca59596e-eb80-40f9-921f-6d67f6e6d8fa" width="72px" /> -->
|
|
||||||
|
|
||||||
# OpenPipe
|
# OpenPipe
|
||||||
|
|
||||||
OpenPipe is a flexible playground for comparing and optimizing LLM prompts. It lets you quickly generate, test and compare candidate prompts, and can automatically [translate](#-translate-between-model-apis) those prompts between models.
|
OpenPipe is a flexible playground for comparing and optimizing LLM prompts. It lets you quickly generate, test and compare candidate prompts, and can automatically [translate](#-translate-between-model-apis) those prompts between models.
|
||||||
|
|
||||||
<img src="https://github.com/openpipe/openpipe/assets/41524992/219a844e-3f4e-4f6b-8066-41348b42977b" alt="demo">
|
<img src="https://github.com/openpipe/openpipe/assets/41524992/66bb1843-cb72-4130-a369-eec2df3b8201" alt="demo">
|
||||||
|
|
||||||
You can use our hosted version of OpenPipe at https://openpipe.ai. You can also clone this repository and [run it locally](#running-locally).
|
You can use our hosted version of OpenPipe at https://openpipe.ai. You can also clone this repository and [run it locally](#running-locally).
|
||||||
|
|
||||||
@@ -37,25 +35,19 @@ OpenPipe lets you _template_ a prompt. Use the templating feature to run the pro
|
|||||||
|
|
||||||
Write your prompt in one format and automatically convert it to work with any other model.
|
Write your prompt in one format and automatically convert it to work with any other model.
|
||||||
|
|
||||||
<img width="480" alt="Screenshot 2023-08-01 at 11 55 38 PM" src="https://github.com/OpenPipe/OpenPipe/assets/41524992/1e19ccf2-96b6-4e93-a3a5-1449710d1b5b" alt="translate between models">
|
<!-- <img width="480" alt="Screenshot 2023-08-01 at 11 55 38 PM" src="https://github.com/OpenPipe/OpenPipe/assets/41524992/1e19ccf2-96b6-4e93-a3a5-1449710d1b5b" alt="translate between models"> -->
|
||||||
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
### 🛠️ Refine Your Prompts Automatically
|
### 🛠️ Refine Your Prompts Automatically
|
||||||
|
|
||||||
Use a growing database of best-practice refinements to improve your prompts automatically.
|
Use a growing database of best-practice refinements to improve your prompts automatically.
|
||||||
|
|
||||||
<img width="480" alt="Screenshot 2023-08-01 at 11 55 38 PM" src="https://github.com/OpenPipe/OpenPipe/assets/41524992/87a27fe7-daef-445c-a5e2-1c82b23f9f99" alt="add function call">
|
<!-- <img width="480" alt="Screenshot 2023-08-01 at 11 55 38 PM" src="https://github.com/OpenPipe/OpenPipe/assets/41524992/87a27fe7-daef-445c-a5e2-1c82b23f9f99" alt="add function call"> -->
|
||||||
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
### 🪄 Auto-generate Test Scenarios
|
### 🪄 Auto-generate Test Scenarios
|
||||||
|
|
||||||
OpenPipe includes a tool to generate new test scenarios based on your existing prompts and scenarios. Just click "Autogenerate Scenario" to try it out!
|
OpenPipe includes a tool to generate new test scenarios based on your existing prompts and scenarios. Just click "Autogenerate Scenario" to try it out!
|
||||||
|
|
||||||
<img width="600" src="https://github.com/openpipe/openpipe/assets/41524992/219a844e-3f4e-4f6b-8066-41348b42977b" alt="auto-generate">
|
<!-- <img width="600" src="https://github.com/openpipe/openpipe/assets/41524992/219a844e-3f4e-4f6b-8066-41348b42977b" alt="auto-generate"> -->
|
||||||
|
|
||||||
<br><br>
|
|
||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
|
|
||||||
|
|||||||
@@ -32,5 +32,11 @@ NEXT_PUBLIC_HOST="http://localhost:3000"
|
|||||||
GITHUB_CLIENT_ID="your_client_id"
|
GITHUB_CLIENT_ID="your_client_id"
|
||||||
GITHUB_CLIENT_SECRET="your_secret"
|
GITHUB_CLIENT_SECRET="your_secret"
|
||||||
|
|
||||||
OPENPIPE_BASE_URL="http://localhost:3000/api"
|
OPENPIPE_BASE_URL="http://localhost:3000/api/v1"
|
||||||
OPENPIPE_API_KEY="your_key"
|
OPENPIPE_API_KEY="your_key"
|
||||||
|
|
||||||
|
SENDER_EMAIL="placeholder"
|
||||||
|
SMTP_HOST="placeholder"
|
||||||
|
SMTP_PORT="placeholder"
|
||||||
|
SMTP_LOGIN="placeholder"
|
||||||
|
SMTP_PASSWORD="placeholder"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const config = {
|
|||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"],
|
extends: ["plugin:@typescript-eslint/recommended-requiring-type-checking"],
|
||||||
files: ["*.ts", "*.tsx"],
|
files: ["*.mts", "*.ts", "*.tsx"],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: path.join(__dirname, "tsconfig.json"),
|
project: path.join(__dirname, "tsconfig.json"),
|
||||||
},
|
},
|
||||||
|
|||||||
3
app/.gitignore
vendored
3
app/.gitignore
vendored
@@ -44,3 +44,6 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Sentry Auth Token
|
# Sentry Auth Token
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
|
||||||
|
# custom openai intialization
|
||||||
|
src/server/utils/openaiCustomConfig.json
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
*.schema.json
|
|
||||||
pnpm-lock.yaml
|
|
||||||
15
app/@types/nextjs-routes.d.ts
vendored
15
app/@types/nextjs-routes.d.ts
vendored
@@ -12,19 +12,20 @@ declare module "nextjs-routes" {
|
|||||||
|
|
||||||
export type Route =
|
export type Route =
|
||||||
| StaticRoute<"/account/signin">
|
| StaticRoute<"/account/signin">
|
||||||
| DynamicRoute<"/api/[...trpc]", { "trpc": string[] }>
|
| StaticRoute<"/admin/jobs">
|
||||||
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
| DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }>
|
||||||
| StaticRoute<"/api/experiments/og-image">
|
| StaticRoute<"/api/experiments/og-image">
|
||||||
| StaticRoute<"/api/openapi">
|
|
||||||
| StaticRoute<"/api/sentry-example-api">
|
|
||||||
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
| DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }>
|
||||||
| DynamicRoute<"/data/[id]", { "id": string }>
|
| DynamicRoute<"/api/v1/[...trpc]", { "trpc": string[] }>
|
||||||
| StaticRoute<"/data">
|
| StaticRoute<"/api/v1/openapi">
|
||||||
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
| StaticRoute<"/dashboard">
|
||||||
|
| DynamicRoute<"/experiments/[experimentSlug]", { "experimentSlug": string }>
|
||||||
| StaticRoute<"/experiments">
|
| StaticRoute<"/experiments">
|
||||||
|
| StaticRoute<"/fine-tunes">
|
||||||
| StaticRoute<"/">
|
| StaticRoute<"/">
|
||||||
| StaticRoute<"/logged-calls">
|
| DynamicRoute<"/invitations/[invitationToken]", { "invitationToken": string }>
|
||||||
| StaticRoute<"/project/settings">
|
| StaticRoute<"/project/settings">
|
||||||
|
| StaticRoute<"/request-logs">
|
||||||
| StaticRoute<"/sentry-example-page">
|
| StaticRoute<"/sentry-example-page">
|
||||||
| StaticRoute<"/world-champs">
|
| StaticRoute<"/world-champs">
|
||||||
| StaticRoute<"/world-champs/signup">;
|
| StaticRoute<"/world-champs/signup">;
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ RUN yarn global add pnpm
|
|||||||
# DEPS
|
# DEPS
|
||||||
FROM base as deps
|
FROM base as deps
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /code
|
||||||
|
|
||||||
COPY prisma ./
|
COPY app/prisma app/package.json ./app/
|
||||||
|
COPY client-libs/typescript/package.json ./client-libs/typescript/
|
||||||
|
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
RUN cd app && pnpm install --frozen-lockfile
|
||||||
|
|
||||||
RUN pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
# BUILDER
|
# BUILDER
|
||||||
FROM base as builder
|
FROM base as builder
|
||||||
@@ -23,24 +23,25 @@ ARG NEXT_PUBLIC_SOCKET_URL
|
|||||||
ARG NEXT_PUBLIC_HOST
|
ARG NEXT_PUBLIC_HOST
|
||||||
ARG NEXT_PUBLIC_SENTRY_DSN
|
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||||
ARG SENTRY_AUTH_TOKEN
|
ARG SENTRY_AUTH_TOKEN
|
||||||
ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /code
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /code/node_modules ./node_modules
|
||||||
|
COPY --from=deps /code/app/node_modules ./app/node_modules
|
||||||
|
COPY --from=deps /code/client-libs/typescript/node_modules ./client-libs/typescript/node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN SKIP_ENV_VALIDATION=1 pnpm build
|
RUN cd app && SKIP_ENV_VALIDATION=1 pnpm build
|
||||||
|
|
||||||
# RUNNER
|
# RUNNER
|
||||||
FROM base as runner
|
FROM base as runner
|
||||||
WORKDIR /app
|
WORKDIR /code/app
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
COPY --from=builder /app/ ./
|
COPY --from=builder /code/ /code/
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
ENV PORT 3000
|
ENV PORT 3000
|
||||||
|
|
||||||
# Run the "run-prod.sh" script
|
# Run the "run-prod.sh" script
|
||||||
CMD /app/run-prod.sh
|
CMD /code/app/scripts/run-prod.sh
|
||||||
@@ -36,6 +36,8 @@ let config = {
|
|||||||
});
|
});
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
transpilePackages: ["openpipe"],
|
||||||
};
|
};
|
||||||
|
|
||||||
config = nextRoutes()(config);
|
config = nextRoutes()(config);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "openpipe",
|
"name": "openpipe-app",
|
||||||
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -9,14 +10,15 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev:next": "next dev",
|
"dev:next": "TZ=UTC next dev",
|
||||||
"dev:wss": "pnpm tsx --watch src/wss-server.ts",
|
"dev:wss": "pnpm tsx --watch src/wss-server.ts",
|
||||||
"dev:worker": "NODE_ENV='development' pnpm tsx --watch src/server/tasks/worker.ts",
|
"worker": "NODE_ENV='development' pnpm tsx --watch src/server/tasks/worker.ts",
|
||||||
"dev": "concurrently --kill-others 'pnpm dev:next' 'pnpm dev:wss' 'pnpm dev:worker'",
|
"dev": "concurrently --kill-others 'pnpm dev:next' 'pnpm dev:wss' 'pnpm worker --watch'",
|
||||||
"postinstall": "prisma generate",
|
"postinstall": "prisma generate",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start": "next start",
|
"start": "TZ=UTC next start",
|
||||||
"codegen": "tsx src/server/scripts/client-codegen.ts",
|
"codegen:clients": "tsx src/server/scripts/client-codegen.ts",
|
||||||
|
"codegen:db": "prisma generate && kysely-codegen --dialect postgres --out-file src/server/db.types.ts",
|
||||||
"seed": "tsx prisma/seed.ts",
|
"seed": "tsx prisma/seed.ts",
|
||||||
"check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'",
|
"check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'",
|
||||||
"test": "pnpm vitest"
|
"test": "pnpm vitest"
|
||||||
@@ -24,7 +26,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.5.8",
|
"@anthropic-ai/sdk": "^0.5.8",
|
||||||
"@apidevtools/json-schema-ref-parser": "^10.1.0",
|
"@apidevtools/json-schema-ref-parser": "^10.1.0",
|
||||||
"@babel/preset-typescript": "^7.22.5",
|
|
||||||
"@babel/standalone": "^7.22.9",
|
"@babel/standalone": "^7.22.9",
|
||||||
"@chakra-ui/anatomy": "^2.2.0",
|
"@chakra-ui/anatomy": "^2.2.0",
|
||||||
"@chakra-ui/next-js": "^2.1.4",
|
"@chakra-ui/next-js": "^2.1.4",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"@monaco-editor/loader": "^1.3.3",
|
"@monaco-editor/loader": "^1.3.3",
|
||||||
"@next-auth/prisma-adapter": "^1.0.5",
|
"@next-auth/prisma-adapter": "^1.0.5",
|
||||||
"@prisma/client": "^4.14.0",
|
"@prisma/client": "^4.14.0",
|
||||||
|
"@sendinblue/client": "^3.3.1",
|
||||||
"@sentry/nextjs": "^7.61.0",
|
"@sentry/nextjs": "^7.61.0",
|
||||||
"@t3-oss/env-nextjs": "^0.3.1",
|
"@t3-oss/env-nextjs": "^0.3.1",
|
||||||
"@tabler/icons-react": "^2.22.0",
|
"@tabler/icons-react": "^2.22.0",
|
||||||
@@ -46,6 +48,7 @@
|
|||||||
"@trpc/react-query": "^10.26.0",
|
"@trpc/react-query": "^10.26.0",
|
||||||
"@trpc/server": "^10.26.0",
|
"@trpc/server": "^10.26.0",
|
||||||
"@vercel/og": "^0.5.9",
|
"@vercel/og": "^0.5.9",
|
||||||
|
"archiver": "^6.0.0",
|
||||||
"ast-types": "^0.14.2",
|
"ast-types": "^0.14.2",
|
||||||
"chroma-js": "^2.4.2",
|
"chroma-js": "^2.4.2",
|
||||||
"concurrently": "^8.2.0",
|
"concurrently": "^8.2.0",
|
||||||
@@ -58,20 +61,25 @@
|
|||||||
"framer-motion": "^10.12.17",
|
"framer-motion": "^10.12.17",
|
||||||
"gpt-tokens": "^1.0.10",
|
"gpt-tokens": "^1.0.10",
|
||||||
"graphile-worker": "^0.13.0",
|
"graphile-worker": "^0.13.0",
|
||||||
|
"human-id": "^4.0.0",
|
||||||
"immer": "^10.0.2",
|
"immer": "^10.0.2",
|
||||||
"isolated-vm": "^4.5.0",
|
"isolated-vm": "^4.5.0",
|
||||||
"json-schema-to-typescript": "^13.0.2",
|
"json-schema-to-typescript": "^13.0.2",
|
||||||
"json-stringify-pretty-compact": "^4.0.0",
|
"json-stringify-pretty-compact": "^4.0.0",
|
||||||
"jsonschema": "^1.4.1",
|
"jsonschema": "^1.4.1",
|
||||||
"kysely": "^0.26.1",
|
"kysely": "^0.26.1",
|
||||||
|
"kysely-codegen": "^0.10.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-react": "^0.265.0",
|
"lucide-react": "^0.265.0",
|
||||||
|
"marked": "^7.0.3",
|
||||||
"next": "^13.4.2",
|
"next": "^13.4.2",
|
||||||
"next-auth": "^4.22.1",
|
"next-auth": "^4.22.1",
|
||||||
"next-query-params": "^4.2.3",
|
"next-query-params": "^4.2.3",
|
||||||
"nextjs-cors": "^2.1.2",
|
"nextjs-cors": "^2.1.2",
|
||||||
"nextjs-routes": "^2.0.1",
|
"nextjs-routes": "^2.0.1",
|
||||||
|
"nodemailer": "^6.9.4",
|
||||||
"openai": "4.0.0-beta.7",
|
"openai": "4.0.0-beta.7",
|
||||||
|
"openpipe": "workspace:*",
|
||||||
"pg": "^8.11.2",
|
"pg": "^8.11.2",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"posthog-js": "^1.75.3",
|
"posthog-js": "^1.75.3",
|
||||||
@@ -92,6 +100,7 @@
|
|||||||
"replicate": "^0.12.3",
|
"replicate": "^0.12.3",
|
||||||
"socket.io": "^4.7.1",
|
"socket.io": "^4.7.1",
|
||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
|
"stream-buffers": "^3.0.2",
|
||||||
"superjson": "1.12.2",
|
"superjson": "1.12.2",
|
||||||
"trpc-openapi": "^1.2.0",
|
"trpc-openapi": "^1.2.0",
|
||||||
"tsx": "^3.12.7",
|
"tsx": "^3.12.7",
|
||||||
@@ -104,6 +113,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",
|
"@openapi-contrib/openapi-schema-to-json-schema": "^4.0.5",
|
||||||
|
"@types/archiver": "^5.3.2",
|
||||||
"@types/babel__core": "^7.20.1",
|
"@types/babel__core": "^7.20.1",
|
||||||
"@types/babel__standalone": "^7.1.4",
|
"@types/babel__standalone": "^7.1.4",
|
||||||
"@types/chroma-js": "^2.4.0",
|
"@types/chroma-js": "^2.4.0",
|
||||||
@@ -113,12 +123,14 @@
|
|||||||
"@types/json-schema": "^7.0.12",
|
"@types/json-schema": "^7.0.12",
|
||||||
"@types/lodash-es": "^4.17.8",
|
"@types/lodash-es": "^4.17.8",
|
||||||
"@types/node": "^18.16.0",
|
"@types/node": "^18.16.0",
|
||||||
|
"@types/nodemailer": "^6.4.9",
|
||||||
"@types/pg": "^8.10.2",
|
"@types/pg": "^8.10.2",
|
||||||
"@types/pluralize": "^0.0.30",
|
"@types/pluralize": "^0.0.30",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react": "^18.2.6",
|
"@types/react": "^18.2.6",
|
||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
"@types/react-syntax-highlighter": "^15.5.7",
|
"@types/react-syntax-highlighter": "^15.5.7",
|
||||||
|
"@types/stream-buffers": "^3.0.4",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||||
"@typescript-eslint/parser": "^5.59.6",
|
"@typescript-eslint/parser": "^5.59.6",
|
||||||
@@ -128,6 +140,7 @@
|
|||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"monaco-editor": "^0.40.0",
|
"monaco-editor": "^0.40.0",
|
||||||
"openapi-typescript": "^6.3.4",
|
"openapi-typescript": "^6.3.4",
|
||||||
|
"openapi-typescript-codegen": "^0.25.0",
|
||||||
"prisma": "^4.14.0",
|
"prisma": "^4.14.0",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LoggedCall" ADD COLUMN "model" TEXT;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "LoggedCallTag_name_idx";
|
||||||
|
DROP INDEX "LoggedCallTag_name_value_idx";
|
||||||
|
|
||||||
|
-- AlterTable: Add projectId column without NOT NULL constraint for now
|
||||||
|
ALTER TABLE "LoggedCallTag" ADD COLUMN "projectId" UUID;
|
||||||
|
|
||||||
|
-- Set the default value
|
||||||
|
UPDATE "LoggedCallTag" lct
|
||||||
|
SET "projectId" = lc."projectId"
|
||||||
|
FROM "LoggedCall" lc
|
||||||
|
WHERE lct."loggedCallId" = lc.id;
|
||||||
|
|
||||||
|
-- Now set the NOT NULL constraint
|
||||||
|
ALTER TABLE "LoggedCallTag" ALTER COLUMN "projectId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LoggedCallTag_projectId_name_idx" ON "LoggedCallTag"("projectId", "name");
|
||||||
|
CREATE INDEX "LoggedCallTag_projectId_name_value_idx" ON "LoggedCallTag"("projectId", "name", "value");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "LoggedCallTag_loggedCallId_name_key" ON "LoggedCallTag"("loggedCallId", "name");
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserInvitation" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"projectId" UUID NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"role" "ProjectUserRole" NOT NULL,
|
||||||
|
"invitationToken" TEXT NOT NULL,
|
||||||
|
"senderId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "UserInvitation_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UserInvitation_invitationToken_key" ON "UserInvitation"("invitationToken");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UserInvitation_projectId_email_key" ON "UserInvitation"("projectId", "email");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserInvitation" ADD CONSTRAINT "UserInvitation_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserInvitation" ADD CONSTRAINT "UserInvitation_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 Viascom Ltd liab. Co
|
||||||
|
*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION nanoid(
|
||||||
|
size int DEFAULT 21,
|
||||||
|
alphabet text DEFAULT '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||||
|
)
|
||||||
|
RETURNS text
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
volatile
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
idBuilder text := '';
|
||||||
|
counter int := 0;
|
||||||
|
bytes bytea;
|
||||||
|
alphabetIndex int;
|
||||||
|
alphabetArray text[];
|
||||||
|
alphabetLength int;
|
||||||
|
mask int;
|
||||||
|
step int;
|
||||||
|
BEGIN
|
||||||
|
alphabetArray := regexp_split_to_array(alphabet, '');
|
||||||
|
alphabetLength := array_length(alphabetArray, 1);
|
||||||
|
mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) as int)) - 1;
|
||||||
|
step := cast(ceil(1.6 * mask * size / alphabetLength) AS int);
|
||||||
|
|
||||||
|
while true
|
||||||
|
loop
|
||||||
|
bytes := gen_random_bytes(step);
|
||||||
|
while counter < step
|
||||||
|
loop
|
||||||
|
alphabetIndex := (get_byte(bytes, counter) & mask) + 1;
|
||||||
|
if alphabetIndex <= alphabetLength then
|
||||||
|
idBuilder := idBuilder || alphabetArray[alphabetIndex];
|
||||||
|
if length(idBuilder) = size then
|
||||||
|
return idBuilder;
|
||||||
|
end if;
|
||||||
|
end if;
|
||||||
|
counter := counter + 1;
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
counter := 0;
|
||||||
|
end loop;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
-- Make a short_nanoid function that uses the default alphabet and length of 15
|
||||||
|
CREATE OR REPLACE FUNCTION short_nanoid()
|
||||||
|
RETURNS text
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
volatile
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
RETURN nanoid(15, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Experiment" ADD COLUMN "slug" TEXT NOT NULL DEFAULT short_nanoid();
|
||||||
|
|
||||||
|
-- For existing experiments, keep the existing id as the slug for backwards compatibility
|
||||||
|
UPDATE "Experiment" SET "slug" = "id";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Experiment_slug_key" ON "Experiment"("slug");
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `input` on the `DatasetEntry` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `output` on the `DatasetEntry` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `loggedCallId` to the `DatasetEntry` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DatasetEntry" DROP COLUMN "input",
|
||||||
|
DROP COLUMN "output",
|
||||||
|
ADD COLUMN "loggedCallId" UUID NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DatasetEntry" ADD CONSTRAINT "DatasetEntry_loggedCallId_fkey" FOREIGN KEY ("loggedCallId") REFERENCES "LoggedCall"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LoggedCallModelResponse" ALTER COLUMN "cost" SET DATA TYPE DOUBLE PRECISION;
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "FineTuneStatus" AS ENUM ('PENDING', 'TRAINING', 'AWAITING_DEPLOYMENT', 'DEPLOYING', 'DEPLOYED', 'ERROR');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "FineTune" (
|
||||||
|
"id" UUID NOT NULL,
|
||||||
|
"slug" TEXT NOT NULL,
|
||||||
|
"baseModel" TEXT NOT NULL,
|
||||||
|
"status" "FineTuneStatus" NOT NULL DEFAULT 'PENDING',
|
||||||
|
"trainingStartedAt" TIMESTAMP(3),
|
||||||
|
"trainingFinishedAt" TIMESTAMP(3),
|
||||||
|
"deploymentStartedAt" TIMESTAMP(3),
|
||||||
|
"deploymentFinishedAt" TIMESTAMP(3),
|
||||||
|
"datasetId" UUID NOT NULL,
|
||||||
|
"projectId" UUID NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "FineTune_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "FineTune_slug_key" ON "FineTune"("slug");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "FineTune" ADD CONSTRAINT "FineTune_datasetId_fkey" FOREIGN KEY ("datasetId") REFERENCES "Dataset"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "FineTune" ADD CONSTRAINT "FineTune_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -11,7 +11,9 @@ datasource db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Experiment {
|
model Experiment {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
|
slug String @unique @default(dbgenerated("short_nanoid()"))
|
||||||
label String
|
label String
|
||||||
|
|
||||||
sortIndex Int @default(0)
|
sortIndex Int @default(0)
|
||||||
@@ -112,17 +114,17 @@ model ScenarioVariantCell {
|
|||||||
model ModelResponse {
|
model ModelResponse {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
cacheKey String
|
cacheKey String
|
||||||
requestedAt DateTime?
|
requestedAt DateTime?
|
||||||
receivedAt DateTime?
|
receivedAt DateTime?
|
||||||
respPayload Json?
|
respPayload Json?
|
||||||
cost Float?
|
cost Float?
|
||||||
inputTokens Int?
|
inputTokens Int?
|
||||||
outputTokens Int?
|
outputTokens Int?
|
||||||
statusCode Int?
|
statusCode Int?
|
||||||
errorMessage String?
|
errorMessage String?
|
||||||
retryTime DateTime?
|
retryTime DateTime?
|
||||||
outdated Boolean @default(false)
|
outdated Boolean @default(false)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -179,6 +181,7 @@ model Dataset {
|
|||||||
|
|
||||||
name String
|
name String
|
||||||
datasetEntries DatasetEntry[]
|
datasetEntries DatasetEntry[]
|
||||||
|
fineTunes FineTune[]
|
||||||
|
|
||||||
projectId String @db.Uuid
|
projectId String @db.Uuid
|
||||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
@@ -190,8 +193,8 @@ model Dataset {
|
|||||||
model DatasetEntry {
|
model DatasetEntry {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
input String
|
loggedCallId String @db.Uuid
|
||||||
output String?
|
loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
datasetId String @db.Uuid
|
datasetId String @db.Uuid
|
||||||
dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: Cascade)
|
dataset Dataset? @relation(fields: [datasetId], references: [id], onDelete: Cascade)
|
||||||
@@ -207,13 +210,15 @@ model Project {
|
|||||||
personalProjectUserId String? @unique @db.Uuid
|
personalProjectUserId String? @unique @db.Uuid
|
||||||
personalProjectUser User? @relation(fields: [personalProjectUserId], references: [id], onDelete: Cascade)
|
personalProjectUser User? @relation(fields: [personalProjectUserId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
projectUsers ProjectUser[]
|
projectUsers ProjectUser[]
|
||||||
experiments Experiment[]
|
projectUserInvitations UserInvitation[]
|
||||||
datasets Dataset[]
|
experiments Experiment[]
|
||||||
loggedCalls LoggedCall[]
|
datasets Dataset[]
|
||||||
apiKeys ApiKey[]
|
loggedCalls LoggedCall[]
|
||||||
|
fineTunes FineTune[]
|
||||||
|
apiKeys ApiKey[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ProjectUserRole {
|
enum ProjectUserRole {
|
||||||
@@ -273,7 +278,9 @@ model LoggedCall {
|
|||||||
projectId String @db.Uuid
|
projectId String @db.Uuid
|
||||||
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
tags LoggedCallTag[]
|
model String?
|
||||||
|
tags LoggedCallTag[]
|
||||||
|
datasetEntries DatasetEntry[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -294,7 +301,7 @@ model LoggedCallModelResponse {
|
|||||||
errorMessage String?
|
errorMessage String?
|
||||||
|
|
||||||
requestedAt DateTime
|
requestedAt DateTime
|
||||||
receivedAt DateTime
|
receivedAt DateTime
|
||||||
|
|
||||||
// Note: the function to calculate the cacheKey should include the project
|
// Note: the function to calculate the cacheKey should include the project
|
||||||
// ID so we don't share cached responses between projects, which could be an
|
// ID so we don't share cached responses between projects, which could be an
|
||||||
@@ -308,7 +315,7 @@ model LoggedCallModelResponse {
|
|||||||
outputTokens Int?
|
outputTokens Int?
|
||||||
finishReason String?
|
finishReason String?
|
||||||
completionId String?
|
completionId String?
|
||||||
cost Decimal? @db.Decimal(18, 12)
|
cost Float?
|
||||||
|
|
||||||
// The LoggedCall that created this LoggedCallModelResponse
|
// The LoggedCall that created this LoggedCallModelResponse
|
||||||
originalLoggedCallId String @unique @db.Uuid
|
originalLoggedCallId String @unique @db.Uuid
|
||||||
@@ -322,15 +329,17 @@ model LoggedCallModelResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model LoggedCallTag {
|
model LoggedCallTag {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
name String
|
name String
|
||||||
value String?
|
value String?
|
||||||
|
projectId String @db.Uuid
|
||||||
|
|
||||||
loggedCallId String @db.Uuid
|
loggedCallId String @db.Uuid
|
||||||
loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([name])
|
@@unique([loggedCallId, name])
|
||||||
@@index([name, value])
|
@@index([projectId, name])
|
||||||
|
@@index([projectId, name, value])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ApiKey {
|
model ApiKey {
|
||||||
@@ -339,8 +348,8 @@ model ApiKey {
|
|||||||
name String
|
name String
|
||||||
apiKey String @unique
|
apiKey String @unique
|
||||||
|
|
||||||
projectId String @db.Uuid
|
projectId String @db.Uuid
|
||||||
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -387,16 +396,33 @@ model User {
|
|||||||
|
|
||||||
role UserRole @default(USER)
|
role UserRole @default(USER)
|
||||||
|
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
sessions Session[]
|
sessions Session[]
|
||||||
projectUsers ProjectUser[]
|
projectUsers ProjectUser[]
|
||||||
projects Project[]
|
projects Project[]
|
||||||
worldChampEntrant WorldChampEntrant?
|
worldChampEntrant WorldChampEntrant?
|
||||||
|
sentUserInvitations UserInvitation[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model UserInvitation {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
|
projectId String @db.Uuid
|
||||||
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
email String
|
||||||
|
role ProjectUserRole
|
||||||
|
invitationToken String @unique
|
||||||
|
senderId String @db.Uuid
|
||||||
|
sender User @relation(fields: [senderId], references: [id], onDelete: Cascade)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([projectId, email])
|
||||||
|
}
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
identifier String
|
identifier String
|
||||||
token String @unique
|
token String @unique
|
||||||
@@ -404,3 +430,33 @@ model VerificationToken {
|
|||||||
|
|
||||||
@@unique([identifier, token])
|
@@unique([identifier, token])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FineTuneStatus {
|
||||||
|
PENDING
|
||||||
|
TRAINING
|
||||||
|
AWAITING_DEPLOYMENT
|
||||||
|
DEPLOYING
|
||||||
|
DEPLOYED
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
model FineTune {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
|
slug String @unique
|
||||||
|
baseModel String
|
||||||
|
status FineTuneStatus @default(PENDING)
|
||||||
|
trainingStartedAt DateTime?
|
||||||
|
trainingFinishedAt DateTime?
|
||||||
|
deploymentStartedAt DateTime?
|
||||||
|
deploymentFinishedAt DateTime?
|
||||||
|
|
||||||
|
datasetId String @db.Uuid
|
||||||
|
dataset Dataset @relation(fields: [datasetId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
projectId String @db.Uuid
|
||||||
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { prisma } from "~/server/db";
|
|||||||
import dedent from "dedent";
|
import dedent from "dedent";
|
||||||
import { generateNewCell } from "~/server/utils/generateNewCell";
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
import { promptConstructorVersion } from "~/promptConstructor/version";
|
import { promptConstructorVersion } from "~/promptConstructor/version";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
const defaultId = "11111111-1111-1111-1111-111111111111";
|
const defaultId = "11111111-1111-1111-1111-111111111111";
|
||||||
|
|
||||||
@@ -9,6 +10,14 @@ await prisma.project.deleteMany({
|
|||||||
where: { id: defaultId },
|
where: { id: defaultId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mark all users as admins
|
||||||
|
await prisma.user.updateMany({
|
||||||
|
where: {},
|
||||||
|
data: {
|
||||||
|
role: "ADMIN",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// If there's an existing project, just seed into it
|
// If there's an existing project, just seed into it
|
||||||
const project =
|
const project =
|
||||||
(await prisma.project.findFirst({})) ??
|
(await prisma.project.findFirst({})) ??
|
||||||
@@ -16,6 +25,20 @@ const project =
|
|||||||
data: { id: defaultId },
|
data: { id: defaultId },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (env.OPENPIPE_API_KEY) {
|
||||||
|
await prisma.apiKey.upsert({
|
||||||
|
where: {
|
||||||
|
apiKey: env.OPENPIPE_API_KEY,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
projectId: project.id,
|
||||||
|
name: "Default API Key",
|
||||||
|
apiKey: env.OPENPIPE_API_KEY,
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.experiment.deleteMany({
|
await prisma.experiment.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
id: defaultId,
|
id: defaultId,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: number;
|
inputTokens: number;
|
||||||
outputTokens: number;
|
outputTokens: number;
|
||||||
finishReason: string;
|
finishReason: string;
|
||||||
|
tags: { name: string; value: string }[];
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -107,6 +108,7 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 236,
|
inputTokens: 236,
|
||||||
outputTokens: 5,
|
outputTokens: 5,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
|
tags: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -193,6 +195,7 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 222,
|
inputTokens: 222,
|
||||||
outputTokens: 5,
|
outputTokens: 5,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
|
tags: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -231,6 +234,7 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 14,
|
inputTokens: 14,
|
||||||
outputTokens: 7,
|
outputTokens: 7,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
|
tags: [{ name: "prompt_id", value: "id2" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -306,6 +310,10 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 2802,
|
inputTokens: 2802,
|
||||||
outputTokens: 108,
|
outputTokens: 108,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
|
tags: [
|
||||||
|
{ name: "prompt_id", value: "chatcmpl-7lQS3MktOT8BTgNEytl9dkyssCQqL" },
|
||||||
|
{ name: "some_other_tag", value: "some_other_value" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -349,6 +357,7 @@ for (let i = 0; i < 1437; i++) {
|
|||||||
cacheHit: false,
|
cacheHit: false,
|
||||||
requestedAt,
|
requestedAt,
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
|
model: template.reqPayload.model,
|
||||||
createdAt: requestedAt,
|
createdAt: requestedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -388,11 +397,14 @@ for (let i = 0; i < 1437; i++) {
|
|||||||
modelResponseId: loggedCallModelResponseId,
|
modelResponseId: loggedCallModelResponseId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
loggedCallTagsToCreate.push({
|
for (const tag of template.tags) {
|
||||||
loggedCallId,
|
loggedCallTagsToCreate.push({
|
||||||
name: "$model",
|
projectId: project.id,
|
||||||
value: template.reqPayload.model,
|
loggedCallId,
|
||||||
});
|
name: tag.name,
|
||||||
|
value: tag.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
|
|||||||
6
app/scripts/debug-prod.sh
Normal file
6
app/scripts/debug-prod.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y htop psql
|
||||||
@@ -10,6 +10,4 @@ pnpm tsx src/promptConstructor/migrate.ts
|
|||||||
|
|
||||||
echo "Starting the server"
|
echo "Starting the server"
|
||||||
|
|
||||||
pnpm concurrently --kill-others \
|
pnpm start
|
||||||
"pnpm start" \
|
|
||||||
"pnpm tsx src/server/tasks/worker.ts"
|
|
||||||
10
app/scripts/run-workers-prod.sh
Executable file
10
app/scripts/run-workers-prod.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Migrating the database"
|
||||||
|
pnpm prisma migrate deploy
|
||||||
|
|
||||||
|
echo "Starting 4 workers"
|
||||||
|
|
||||||
|
pnpm concurrently "pnpm worker" "pnpm worker" "pnpm worker" "pnpm worker"
|
||||||
13
app/scripts/test-docker.sh
Executable file
13
app/scripts/test-docker.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
echo "Env is"
|
||||||
|
echo $ENVIRONMENT
|
||||||
|
|
||||||
|
docker build . --file app/Dockerfile --tag "openpipe-prod"
|
||||||
|
|
||||||
|
# Run the image
|
||||||
|
docker run --env-file app/.env -it --entrypoint "/bin/bash" "openpipe-prod"
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import { isError } from "lodash-es";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
|
|
||||||
if (env.NEXT_PUBLIC_SENTRY_DSN) {
|
if (env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||||
@@ -15,4 +16,10 @@ if (env.NEXT_PUBLIC_SENTRY_DSN) {
|
|||||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
debug: false,
|
debug: false,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Install local debug exception handler for rejected promises
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
const reasonDetails = isError(reason) ? reason?.stack : reason;
|
||||||
|
console.log("Unhandled Rejection at:", reasonDetails);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Textarea, type TextareaProps } from "@chakra-ui/react";
|
import { Textarea, type TextareaProps } from "@chakra-ui/react";
|
||||||
import ResizeTextarea from "react-textarea-autosize";
|
import ResizeTextarea from "react-textarea-autosize";
|
||||||
import React, { useLayoutEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
export const AutoResizeTextarea: React.ForwardRefRenderFunction<
|
export const AutoResizeTextarea: React.ForwardRefRenderFunction<
|
||||||
HTMLTextAreaElement,
|
HTMLTextAreaElement,
|
||||||
TextareaProps & { minRows?: number }
|
TextareaProps & { minRows?: number }
|
||||||
> = ({ minRows = 1, overflowY = "hidden", ...props }, ref) => {
|
> = ({ minRows = 1, overflowY = "hidden", ...props }, ref) => {
|
||||||
const [isRerendered, setIsRerendered] = useState(false);
|
const [isRerendered, setIsRerendered] = useState(false);
|
||||||
useLayoutEffect(() => setIsRerendered(true), []);
|
useEffect(() => setIsRerendered(true), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const ModelStatsCard = ({
|
|||||||
label="Price"
|
label="Price"
|
||||||
info={
|
info={
|
||||||
<Text>
|
<Text>
|
||||||
${model.pricePerSecond.toFixed(3)}
|
${model.pricePerSecond.toFixed(4)}
|
||||||
<Text color="gray.500"> / second</Text>
|
<Text color="gray.500"> / second</Text>
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { HStack, Icon, IconButton, Tooltip, Text } from "@chakra-ui/react";
|
import { HStack, Icon, IconButton, Tooltip, Text, type StackProps } from "@chakra-ui/react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { MdContentCopy } from "react-icons/md";
|
import { MdContentCopy } from "react-icons/md";
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
|
||||||
const CopiableCode = ({ code }: { code: string }) => {
|
const CopiableCode = ({ code, ...rest }: { code: string } & StackProps) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const [copyToClipboard] = useHandledAsyncCallback(async () => {
|
const [copyToClipboard] = useHandledAsyncCallback(async () => {
|
||||||
await navigator.clipboard.writeText(code);
|
await navigator.clipboard.writeText(code);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
}, [code]);
|
}, [code]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
backgroundColor="blackAlpha.800"
|
backgroundColor="blackAlpha.800"
|
||||||
@@ -18,9 +19,19 @@ const CopiableCode = ({ code }: { code: string }) => {
|
|||||||
padding={3}
|
padding={3}
|
||||||
w="full"
|
w="full"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
|
alignItems="flex-start"
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
<Text fontFamily="inconsolata" fontWeight="bold" letterSpacing={0.5} overflowX="auto">
|
<Text
|
||||||
|
fontFamily="inconsolata"
|
||||||
|
fontWeight="bold"
|
||||||
|
letterSpacing={0.5}
|
||||||
|
overflowX="auto"
|
||||||
|
whiteSpace="pre-wrap"
|
||||||
|
>
|
||||||
{code}
|
{code}
|
||||||
|
{/* Necessary for trailing newline to actually be displayed */}
|
||||||
|
{code.endsWith("\n") ? "\n" : ""}
|
||||||
</Text>
|
</Text>
|
||||||
<Tooltip closeOnClick={false} label={copied ? "Copied!" : "Copy to clipboard"}>
|
<Tooltip closeOnClick={false} label={copied ? "Copied!" : "Copy to clipboard"}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|||||||
14
app/src/components/InfoCircle.tsx
Normal file
14
app/src/components/InfoCircle.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Tooltip, Icon, VStack } from "@chakra-ui/react";
|
||||||
|
import { RiInformationFill } from "react-icons/ri";
|
||||||
|
|
||||||
|
const InfoCircle = ({ tooltipText }: { tooltipText: string }) => {
|
||||||
|
return (
|
||||||
|
<Tooltip label={tooltipText} fontSize="sm" shouldWrapChildren maxW={80}>
|
||||||
|
<VStack>
|
||||||
|
<Icon as={RiInformationFill} boxSize={5} color="gray.500" />
|
||||||
|
</VStack>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfoCircle;
|
||||||
91
app/src/components/InputDropdown.tsx
Normal file
91
app/src/components/InputDropdown.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputRightElement,
|
||||||
|
Icon,
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
type InputGroupProps,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { FiChevronDown } from "react-icons/fi";
|
||||||
|
import { BiCheck } from "react-icons/bi";
|
||||||
|
|
||||||
|
type InputDropdownProps<T> = {
|
||||||
|
options: ReadonlyArray<T>;
|
||||||
|
selectedOption: T;
|
||||||
|
onSelect: (option: T) => void;
|
||||||
|
inputGroupProps?: InputGroupProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputDropdown = <T,>({
|
||||||
|
options,
|
||||||
|
selectedOption,
|
||||||
|
onSelect,
|
||||||
|
inputGroupProps,
|
||||||
|
}: InputDropdownProps<T>) => {
|
||||||
|
const popover = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover placement="bottom-start" {...popover}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<InputGroup
|
||||||
|
cursor="pointer"
|
||||||
|
w={(selectedOption as string).length * 14 + 180}
|
||||||
|
{...inputGroupProps}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={selectedOption as string}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function -- controlled input requires onChange
|
||||||
|
onChange={() => {}}
|
||||||
|
cursor="pointer"
|
||||||
|
borderColor={popover.isOpen ? "blue.500" : undefined}
|
||||||
|
_hover={popover.isOpen ? { borderColor: "blue.500" } : undefined}
|
||||||
|
contentEditable={false}
|
||||||
|
// disable focus
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.target.blur();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<InputRightElement>
|
||||||
|
<Icon as={FiChevronDown} />
|
||||||
|
</InputRightElement>
|
||||||
|
</InputGroup>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);" minW={0} w="auto">
|
||||||
|
<VStack spacing={0}>
|
||||||
|
{options?.map((option, index) => (
|
||||||
|
<HStack
|
||||||
|
key={index}
|
||||||
|
as={Button}
|
||||||
|
onClick={() => {
|
||||||
|
onSelect(option);
|
||||||
|
popover.onClose();
|
||||||
|
}}
|
||||||
|
w="full"
|
||||||
|
variant="ghost"
|
||||||
|
justifyContent="space-between"
|
||||||
|
fontWeight="semibold"
|
||||||
|
borderRadius={0}
|
||||||
|
colorScheme="blue"
|
||||||
|
color="black"
|
||||||
|
fontSize="sm"
|
||||||
|
borderBottomWidth={1}
|
||||||
|
>
|
||||||
|
<Text mr={16}>{option as string}</Text>
|
||||||
|
{option === selectedOption && <Icon as={BiCheck} color="blue.500" boxSize={5} />}
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputDropdown;
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
useHandledAsyncCallback,
|
useHandledAsyncCallback,
|
||||||
useVisibleScenarioIds,
|
useVisibleScenarioIds,
|
||||||
} from "~/utils/hooks";
|
} from "~/utils/hooks";
|
||||||
import { cellPadding } from "../constants";
|
import { cellPadding } from "./constants";
|
||||||
import { ActionButton } from "./ScenariosHeader";
|
import { ActionButton } from "./ScenariosHeader";
|
||||||
|
|
||||||
export default function AddVariantButton() {
|
export default function AddVariantButton() {
|
||||||
@@ -33,25 +33,11 @@ export default function AddVariantButton() {
|
|||||||
<Flex w="100%" justifyContent="flex-end">
|
<Flex w="100%" justifyContent="flex-end">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
py={5}
|
py={7}
|
||||||
leftIcon={<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />}
|
leftIcon={<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />}
|
||||||
>
|
>
|
||||||
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
|
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
{/* <Button
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
fontWeight="normal"
|
|
||||||
bgColor="transparent"
|
|
||||||
_hover={{ bgColor: "gray.100" }}
|
|
||||||
px={cellPadding.x}
|
|
||||||
onClick={onClick}
|
|
||||||
height="unset"
|
|
||||||
minH={headerMinHeight}
|
|
||||||
>
|
|
||||||
<Icon as={loading ? Spinner : BsPlus} boxSize={6} mr={loading ? 1 : 0} />
|
|
||||||
<Text display={{ base: "none", md: "flex" }}>Add Variant</Text>
|
|
||||||
</Button> */}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { HStack, Icon, IconButton, Spinner, Tooltip, useDisclosure } from "@chakra-ui/react";
|
import { HStack, Icon, IconButton, Spinner, Tooltip, useDisclosure } from "@chakra-ui/react";
|
||||||
import { BsArrowClockwise, BsInfoCircle } from "react-icons/bs";
|
import { BsArrowClockwise, BsInfoCircle } from "react-icons/bs";
|
||||||
import { useExperimentAccess } from "~/utils/hooks";
|
import { useExperimentAccess } from "~/utils/hooks";
|
||||||
import ExpandedModal from "./PromptModal";
|
import PromptModal from "./PromptModal";
|
||||||
import { type RouterOutputs } from "~/utils/api";
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
|
|
||||||
export const CellOptions = ({
|
export const CellOptions = ({
|
||||||
@@ -32,7 +32,7 @@ export const CellOptions = ({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ExpandedModal cell={cell} disclosure={modalDisclosure} />
|
<PromptModal cell={cell} disclosure={modalDisclosure} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{canModify && (
|
{canModify && (
|
||||||
29
app/src/components/OutputsTable/OutputCell/CellWrapper.tsx
Normal file
29
app/src/components/OutputsTable/OutputCell/CellWrapper.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { type StackProps, VStack } from "@chakra-ui/react";
|
||||||
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
|
import { type Scenario } from "../types";
|
||||||
|
import { CellOptions } from "./CellOptions";
|
||||||
|
import { OutputStats } from "./OutputStats";
|
||||||
|
|
||||||
|
const CellWrapper: React.FC<
|
||||||
|
StackProps & {
|
||||||
|
cell: RouterOutputs["scenarioVariantCells"]["get"] | undefined;
|
||||||
|
hardRefetching: boolean;
|
||||||
|
hardRefetch: () => void;
|
||||||
|
mostRecentResponse:
|
||||||
|
| NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>["modelResponses"][0]
|
||||||
|
| undefined;
|
||||||
|
scenario: Scenario;
|
||||||
|
}
|
||||||
|
> = ({ children, cell, hardRefetching, hardRefetch, mostRecentResponse, scenario, ...props }) => (
|
||||||
|
<VStack w="full" alignItems="flex-start" {...props} px={2} py={2} h="100%">
|
||||||
|
{cell && (
|
||||||
|
<CellOptions refetchingOutput={hardRefetching} refetchOutput={hardRefetch} cell={cell} />
|
||||||
|
)}
|
||||||
|
<VStack w="full" alignItems="flex-start" maxH={500} overflowY="auto" flex={1}>
|
||||||
|
{children}
|
||||||
|
</VStack>
|
||||||
|
{mostRecentResponse && <OutputStats modelResponse={mostRecentResponse} scenario={scenario} />}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CellWrapper;
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import { api } from "~/utils/api";
|
import { Text } from "@chakra-ui/react";
|
||||||
import { type PromptVariant, type Scenario } from "../types";
|
import stringify from "json-stringify-pretty-compact";
|
||||||
import { type StackProps, Text, VStack } from "@chakra-ui/react";
|
import { Fragment, useEffect, useState, type ReactElement } from "react";
|
||||||
import { useScenarioVars, useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
import { docco } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
||||||
import stringify from "json-stringify-pretty-compact";
|
|
||||||
import { type ReactElement, useState, useEffect, Fragment, useCallback } from "react";
|
|
||||||
import useSocket from "~/utils/useSocket";
|
|
||||||
import { OutputStats } from "./OutputStats";
|
|
||||||
import { RetryCountdown } from "./RetryCountdown";
|
|
||||||
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks";
|
||||||
|
import useSocket from "~/utils/useSocket";
|
||||||
|
import { type PromptVariant, type Scenario } from "../types";
|
||||||
|
import CellWrapper from "./CellWrapper";
|
||||||
import { ResponseLog } from "./ResponseLog";
|
import { ResponseLog } from "./ResponseLog";
|
||||||
import { CellOptions } from "./TopActions";
|
import { RetryCountdown } from "./RetryCountdown";
|
||||||
|
|
||||||
const WAITING_MESSAGE_INTERVAL = 20000;
|
const WAITING_MESSAGE_INTERVAL = 20000;
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output";
|
if (!templateHasVariables) disabledReason = "Add a value to the scenario variables to see output";
|
||||||
|
|
||||||
const [refetchInterval, setRefetchInterval] = useState(0);
|
const [refetchInterval, setRefetchInterval] = useState<number | false>(false);
|
||||||
const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery(
|
const { data: cell, isLoading: queryLoading } = api.scenarioVariantCells.get.useQuery(
|
||||||
{ scenarioId: scenario.id, variantId: variant.id },
|
{ scenarioId: scenario.id, variantId: variant.id },
|
||||||
{ refetchInterval },
|
{ refetchInterval },
|
||||||
@@ -44,7 +43,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
type OutputSchema = Parameters<typeof provider.normalizeOutput>[0];
|
type OutputSchema = Parameters<typeof provider.normalizeOutput>[0];
|
||||||
|
|
||||||
const { mutateAsync: hardRefetchMutate } = api.scenarioVariantCells.forceRefetch.useMutation();
|
const { mutateAsync: hardRefetchMutate } = api.scenarioVariantCells.hardRefetch.useMutation();
|
||||||
const [hardRefetch, hardRefetching] = useHandledAsyncCallback(async () => {
|
const [hardRefetch, hardRefetching] = useHandledAsyncCallback(async () => {
|
||||||
await hardRefetchMutate({ scenarioId: scenario.id, variantId: variant.id });
|
await hardRefetchMutate({ scenarioId: scenario.id, variantId: variant.id });
|
||||||
await utils.scenarioVariantCells.get.invalidate({
|
await utils.scenarioVariantCells.get.invalidate({
|
||||||
@@ -64,42 +63,34 @@ export default function OutputCell({
|
|||||||
cell.retrievalStatus === "PENDING" ||
|
cell.retrievalStatus === "PENDING" ||
|
||||||
cell.retrievalStatus === "IN_PROGRESS" ||
|
cell.retrievalStatus === "IN_PROGRESS" ||
|
||||||
hardRefetching;
|
hardRefetching;
|
||||||
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : 0), [awaitingOutput]);
|
|
||||||
|
useEffect(() => setRefetchInterval(awaitingOutput ? 1000 : false), [awaitingOutput]);
|
||||||
|
|
||||||
// TODO: disconnect from socket if we're not streaming anymore
|
// TODO: disconnect from socket if we're not streaming anymore
|
||||||
const streamedMessage = useSocket<OutputSchema>(cell?.id);
|
const streamedMessage = useSocket<OutputSchema>(cell?.id);
|
||||||
|
|
||||||
const mostRecentResponse = cell?.modelResponses[cell.modelResponses.length - 1];
|
const mostRecentResponse = cell?.modelResponses[cell.modelResponses.length - 1];
|
||||||
|
|
||||||
const CellWrapper = useCallback(
|
const wrapperProps: Parameters<typeof CellWrapper>[0] = {
|
||||||
({ children, ...props }: StackProps) => (
|
cell,
|
||||||
<VStack w="full" alignItems="flex-start" {...props} px={2} py={2} h="100%">
|
hardRefetching,
|
||||||
{cell && (
|
hardRefetch,
|
||||||
<CellOptions refetchingOutput={hardRefetching} refetchOutput={hardRefetch} cell={cell} />
|
mostRecentResponse,
|
||||||
)}
|
scenario,
|
||||||
<VStack w="full" alignItems="flex-start" maxH={500} overflowY="auto" flex={1}>
|
};
|
||||||
{children}
|
|
||||||
</VStack>
|
|
||||||
{mostRecentResponse && (
|
|
||||||
<OutputStats modelResponse={mostRecentResponse} scenario={scenario} />
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
),
|
|
||||||
[hardRefetching, hardRefetch, mostRecentResponse, scenario, cell],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!vars) return null;
|
if (!vars) return null;
|
||||||
|
|
||||||
if (!cell && !fetchingOutput)
|
if (!cell && !fetchingOutput)
|
||||||
return (
|
return (
|
||||||
<CellWrapper>
|
<CellWrapper {...wrapperProps}>
|
||||||
<Text color="gray.500">Error retrieving output</Text>
|
<Text color="gray.500">Error retrieving output</Text>
|
||||||
</CellWrapper>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cell && cell.errorMessage) {
|
if (cell && cell.errorMessage) {
|
||||||
return (
|
return (
|
||||||
<CellWrapper>
|
<CellWrapper {...wrapperProps}>
|
||||||
<Text color="red.500">{cell.errorMessage}</Text>
|
<Text color="red.500">{cell.errorMessage}</Text>
|
||||||
</CellWrapper>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
@@ -111,7 +102,12 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (showLogs)
|
if (showLogs)
|
||||||
return (
|
return (
|
||||||
<CellWrapper alignItems="flex-start" fontFamily="inconsolata, monospace" spacing={0}>
|
<CellWrapper
|
||||||
|
{...wrapperProps}
|
||||||
|
alignItems="flex-start"
|
||||||
|
fontFamily="inconsolata, monospace"
|
||||||
|
spacing={0}
|
||||||
|
>
|
||||||
{cell?.jobQueuedAt && <ResponseLog time={cell.jobQueuedAt} title="Job queued" />}
|
{cell?.jobQueuedAt && <ResponseLog time={cell.jobQueuedAt} title="Job queued" />}
|
||||||
{cell?.jobStartedAt && <ResponseLog time={cell.jobStartedAt} title="Job started" />}
|
{cell?.jobStartedAt && <ResponseLog time={cell.jobStartedAt} title="Job started" />}
|
||||||
{cell?.modelResponses?.map((response) => {
|
{cell?.modelResponses?.map((response) => {
|
||||||
@@ -120,8 +116,13 @@ export default function OutputCell({
|
|||||||
? response.receivedAt.getTime()
|
? response.receivedAt.getTime()
|
||||||
: Date.now();
|
: Date.now();
|
||||||
if (response.requestedAt) {
|
if (response.requestedAt) {
|
||||||
numWaitingMessages = Math.floor(
|
numWaitingMessages = Math.min(
|
||||||
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
|
Math.floor(
|
||||||
|
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
|
||||||
|
),
|
||||||
|
// Don't try to render more than 15, it'll use too much CPU and
|
||||||
|
// break the page
|
||||||
|
15,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -168,7 +169,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (mostRecentResponse?.respPayload && normalizedOutput?.type === "json") {
|
if (mostRecentResponse?.respPayload && normalizedOutput?.type === "json") {
|
||||||
return (
|
return (
|
||||||
<CellWrapper>
|
<CellWrapper {...wrapperProps}>
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
customStyle={{ overflowX: "unset", width: "100%", flex: 1 }}
|
customStyle={{ overflowX: "unset", width: "100%", flex: 1 }}
|
||||||
language="json"
|
language="json"
|
||||||
@@ -187,7 +188,7 @@ export default function OutputCell({
|
|||||||
const contentToDisplay = (normalizedOutput?.type === "text" && normalizedOutput.value) || "";
|
const contentToDisplay = (normalizedOutput?.type === "text" && normalizedOutput.value) || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellWrapper>
|
<CellWrapper {...wrapperProps}>
|
||||||
<Text whiteSpace="pre-wrap">{contentToDisplay}</Text>
|
<Text whiteSpace="pre-wrap">{contentToDisplay}</Text>
|
||||||
</CellWrapper>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,30 +5,103 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
type UseDisclosureReturn,
|
type UseDisclosureReturn,
|
||||||
|
Link,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { type RouterOutputs } from "~/utils/api";
|
import { api, type RouterOutputs } from "~/utils/api";
|
||||||
import { JSONTree } from "react-json-tree";
|
import { JSONTree } from "react-json-tree";
|
||||||
|
import CopiableCode from "~/components/CopiableCode";
|
||||||
|
|
||||||
export default function ExpandedModal(props: {
|
const theme = {
|
||||||
|
scheme: "chalk",
|
||||||
|
author: "chris kempson (http://chriskempson.com)",
|
||||||
|
base00: "transparent",
|
||||||
|
base01: "#202020",
|
||||||
|
base02: "#303030",
|
||||||
|
base03: "#505050",
|
||||||
|
base04: "#b0b0b0",
|
||||||
|
base05: "#d0d0d0",
|
||||||
|
base06: "#e0e0e0",
|
||||||
|
base07: "#f5f5f5",
|
||||||
|
base08: "#fb9fb1",
|
||||||
|
base09: "#eda987",
|
||||||
|
base0A: "#ddb26f",
|
||||||
|
base0B: "#acc267",
|
||||||
|
base0C: "#12cfc0",
|
||||||
|
base0D: "#6fc2ef",
|
||||||
|
base0E: "#e1a3ee",
|
||||||
|
base0F: "#deaf8f",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PromptModal(props: {
|
||||||
cell: NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>;
|
cell: NonNullable<RouterOutputs["scenarioVariantCells"]["get"]>;
|
||||||
disclosure: UseDisclosureReturn;
|
disclosure: UseDisclosureReturn;
|
||||||
}) {
|
}) {
|
||||||
|
const { data } = api.scenarioVariantCells.getTemplatedPromptMessage.useQuery(
|
||||||
|
{
|
||||||
|
cellId: props.cell.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: props.disclosure.isOpen,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={props.disclosure.isOpen} onClose={props.disclosure.onClose} size="2xl">
|
<Modal isOpen={props.disclosure.isOpen} onClose={props.disclosure.onClose} size="xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>Prompt</ModalHeader>
|
<ModalHeader>Prompt Details</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<JSONTree
|
<VStack py={4} w="">
|
||||||
data={props.cell.prompt}
|
<VStack w="full" alignItems="flex-start">
|
||||||
invertTheme={true}
|
<Text fontWeight="bold">Full Prompt</Text>
|
||||||
theme="chalk"
|
<Box
|
||||||
shouldExpandNodeInitially={() => true}
|
w="full"
|
||||||
getItemString={() => ""}
|
p={4}
|
||||||
hideRoot
|
alignItems="flex-start"
|
||||||
/>
|
backgroundColor="blackAlpha.800"
|
||||||
|
borderRadius={4}
|
||||||
|
>
|
||||||
|
<JSONTree
|
||||||
|
data={props.cell.prompt}
|
||||||
|
theme={theme}
|
||||||
|
shouldExpandNodeInitially={() => true}
|
||||||
|
getItemString={() => ""}
|
||||||
|
hideRoot
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</VStack>
|
||||||
|
{data?.templatedPrompt && (
|
||||||
|
<VStack w="full" mt={4} alignItems="flex-start">
|
||||||
|
<Text fontWeight="bold">Templated prompt message:</Text>
|
||||||
|
<CopiableCode
|
||||||
|
w="full"
|
||||||
|
// bgColor="gray.100"
|
||||||
|
p={4}
|
||||||
|
borderWidth={1}
|
||||||
|
whiteSpace="pre-wrap"
|
||||||
|
code={data.templatedPrompt}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
{data?.learnMoreUrl && (
|
||||||
|
<Link
|
||||||
|
href={data.learnMoreUrl}
|
||||||
|
isExternal
|
||||||
|
color="blue.500"
|
||||||
|
fontWeight="bold"
|
||||||
|
fontSize="sm"
|
||||||
|
mt={4}
|
||||||
|
alignSelf="flex-end"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
VStack,
|
VStack,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { BsArrowsAngleExpand, BsX } from "react-icons/bs";
|
import { BsArrowsAngleExpand, BsX } from "react-icons/bs";
|
||||||
import { cellPadding } from "../constants";
|
import { cellPadding } from "./constants";
|
||||||
import { FloatingLabelInput } from "./FloatingLabelInput";
|
import { FloatingLabelInput } from "./FloatingLabelInput";
|
||||||
import { ScenarioEditorModal } from "./ScenarioEditorModal";
|
import { ScenarioEditorModal } from "./ScenarioEditorModal";
|
||||||
|
|
||||||
@@ -111,25 +111,23 @@ export default function ScenarioEditor({
|
|||||||
onDrop={onReorder}
|
onDrop={onReorder}
|
||||||
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
|
backgroundColor={isDragTarget ? "gray.100" : "transparent"}
|
||||||
>
|
>
|
||||||
{variableLabels.length === 0 ? (
|
{
|
||||||
<Box color="gray.500">
|
|
||||||
{vars.data ? "No scenario variables configured" : "Loading..."}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<VStack spacing={4} flex={1} py={2}>
|
<VStack spacing={4} flex={1} py={2}>
|
||||||
<HStack justifyContent="space-between" w="100%" align="center" spacing={0}>
|
<HStack justifyContent="space-between" w="100%" align="center" spacing={0}>
|
||||||
<Text flex={1}>Scenario</Text>
|
<Text flex={1}>Scenario</Text>
|
||||||
<Tooltip label="Expand" hasArrow>
|
{variableLabels.length && (
|
||||||
<IconButton
|
<Tooltip label="Expand" hasArrow>
|
||||||
aria-label="Expand"
|
<IconButton
|
||||||
icon={<Icon as={BsArrowsAngleExpand} boxSize={3} />}
|
aria-label="Expand"
|
||||||
onClick={() => setScenarioEditorModalOpen(true)}
|
icon={<Icon as={BsArrowsAngleExpand} boxSize={3} />}
|
||||||
size="xs"
|
onClick={() => setScenarioEditorModalOpen(true)}
|
||||||
colorScheme="gray"
|
size="xs"
|
||||||
color="gray.500"
|
colorScheme="gray"
|
||||||
variant="ghost"
|
color="gray.500"
|
||||||
/>
|
variant="ghost"
|
||||||
</Tooltip>
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{canModify && props.canHide && (
|
{canModify && props.canHide && (
|
||||||
<Tooltip label="Delete" hasArrow>
|
<Tooltip label="Delete" hasArrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -150,31 +148,38 @@ export default function ScenarioEditor({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
{variableLabels.map((key) => {
|
|
||||||
const value = values[key] ?? "";
|
{variableLabels.length === 0 ? (
|
||||||
return (
|
<Box color="gray.500">
|
||||||
<FloatingLabelInput
|
{vars.data ? "No scenario variables configured" : "Loading..."}
|
||||||
key={key}
|
</Box>
|
||||||
label={key}
|
) : (
|
||||||
isDisabled={!canModify}
|
variableLabels.map((key) => {
|
||||||
style={{ width: "100%" }}
|
const value = values[key] ?? "";
|
||||||
maxHeight={32}
|
return (
|
||||||
value={value}
|
<FloatingLabelInput
|
||||||
onChange={(e) => {
|
key={key}
|
||||||
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
label={key}
|
||||||
}}
|
isDisabled={!canModify}
|
||||||
onKeyDown={(e) => {
|
style={{ width: "100%" }}
|
||||||
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
maxHeight={32}
|
||||||
e.preventDefault();
|
value={value}
|
||||||
e.currentTarget.blur();
|
onChange={(e) => {
|
||||||
onSave();
|
setValues((prev) => ({ ...prev, [key]: e.target.value }));
|
||||||
}
|
}}
|
||||||
}}
|
onKeyDown={(e) => {
|
||||||
onMouseEnter={() => setVariableInputHovered(true)}
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||||
onMouseLeave={() => setVariableInputHovered(false)}
|
e.preventDefault();
|
||||||
/>
|
e.currentTarget.blur();
|
||||||
);
|
onSave();
|
||||||
})}
|
}
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setVariableInputHovered(true)}
|
||||||
|
onMouseLeave={() => setVariableInputHovered(false)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
{hasChanged && (
|
{hasChanged && (
|
||||||
<HStack justify="right">
|
<HStack justify="right">
|
||||||
<Button
|
<Button
|
||||||
@@ -192,7 +197,7 @@ export default function ScenarioEditor({
|
|||||||
</HStack>
|
</HStack>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
)}
|
}
|
||||||
</HStack>
|
</HStack>
|
||||||
{scenarioEditorModalOpen && (
|
{scenarioEditorModalOpen && (
|
||||||
<ScenarioEditorModal
|
<ScenarioEditorModal
|
||||||
|
|||||||
@@ -65,11 +65,11 @@ export const ScenarioEditorModal = ({
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen
|
isOpen
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
size={{ base: "xl", sm: "2xl", md: "3xl", lg: "5xl", xl: "7xl" }}
|
size={{ base: "xl", sm: "2xl", md: "3xl", lg: "4xl", xl: "5xl" }}
|
||||||
>
|
>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent w={1200}>
|
<ModalContent w={1200}>
|
||||||
<ModalHeader />
|
<ModalHeader>Edit Scenario</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody maxW="unset">
|
<ModalBody maxW="unset">
|
||||||
<VStack spacing={8}>
|
<VStack spacing={8}>
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
|
import { type StackProps } from "@chakra-ui/react";
|
||||||
|
|
||||||
import { useScenarios } from "~/utils/hooks";
|
import { useScenarios } from "~/utils/hooks";
|
||||||
import Paginator from "../Paginator";
|
import Paginator from "../Paginator";
|
||||||
|
|
||||||
const ScenarioPaginator = () => {
|
const ScenarioPaginator = (props: StackProps) => {
|
||||||
const { data } = useScenarios();
|
const { data } = useScenarios();
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const { scenarios, startIndex, lastPage, count } = data;
|
const { count } = data;
|
||||||
|
|
||||||
return (
|
return <Paginator count={count} condense {...props} />;
|
||||||
<Paginator
|
|
||||||
numItemsLoaded={scenarios.length}
|
|
||||||
startIndex={startIndex}
|
|
||||||
lastPage={lastPage}
|
|
||||||
count={count}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ScenarioPaginator;
|
export default ScenarioPaginator;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const ScenarioRow = (props: {
|
|||||||
variants: PromptVariant[];
|
variants: PromptVariant[];
|
||||||
canHide: boolean;
|
canHide: boolean;
|
||||||
rowStart: number;
|
rowStart: number;
|
||||||
|
isFirst: boolean;
|
||||||
|
isLast: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
@@ -21,10 +23,14 @@ const ScenarioRow = (props: {
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
sx={isHovered ? highlightStyle : undefined}
|
sx={isHovered ? highlightStyle : undefined}
|
||||||
borderLeftWidth={1}
|
bgColor="white"
|
||||||
{...borders}
|
|
||||||
rowStart={props.rowStart}
|
rowStart={props.rowStart}
|
||||||
colStart={1}
|
colStart={1}
|
||||||
|
borderLeftWidth={1}
|
||||||
|
borderTopWidth={props.isFirst ? 1 : 0}
|
||||||
|
borderTopLeftRadius={props.isFirst ? 8 : 0}
|
||||||
|
borderBottomLeftRadius={props.isLast ? 8 : 0}
|
||||||
|
{...borders}
|
||||||
>
|
>
|
||||||
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
|
<ScenarioEditor scenario={props.scenario} hovered={isHovered} canHide={props.canHide} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
@@ -34,8 +40,12 @@ const ScenarioRow = (props: {
|
|||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
sx={isHovered ? highlightStyle : undefined}
|
sx={isHovered ? highlightStyle : undefined}
|
||||||
|
bgColor="white"
|
||||||
rowStart={props.rowStart}
|
rowStart={props.rowStart}
|
||||||
colStart={i + 2}
|
colStart={i + 2}
|
||||||
|
borderTopWidth={props.isFirst ? 1 : 0}
|
||||||
|
borderTopRightRadius={props.isFirst && i === props.variants.length - 1 ? 8 : 0}
|
||||||
|
borderBottomRightRadius={props.isLast && i === props.variants.length - 1 ? 8 : 0}
|
||||||
{...borders}
|
{...borders}
|
||||||
>
|
>
|
||||||
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
|
<OutputCell key={variant.id} scenario={props.scenario} variant={variant} />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { cellPadding } from "../constants";
|
import { cellPadding } from "./constants";
|
||||||
import {
|
import {
|
||||||
useExperiment,
|
useExperiment,
|
||||||
useExperimentAccess,
|
useExperimentAccess,
|
||||||
@@ -48,7 +48,7 @@ export const ScenariosHeader = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack w="100%" pb={cellPadding.y} pt={0} align="center" spacing={0}>
|
<HStack w="100%" py={cellPadding.y} px={cellPadding.x} align="center" spacing={0}>
|
||||||
<Text fontSize={16} fontWeight="bold">
|
<Text fontSize={16} fontWeight="bold">
|
||||||
Scenarios ({scenarios.data?.count})
|
Scenarios ({scenarios.data?.count})
|
||||||
</Text>
|
</Text>
|
||||||
@@ -57,11 +57,16 @@ export const ScenariosHeader = () => {
|
|||||||
<MenuButton
|
<MenuButton
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
mt={1}
|
mt={1}
|
||||||
|
ml={2}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
aria-label="Edit Scenarios"
|
aria-label="Edit Scenarios"
|
||||||
icon={<Icon as={loading ? Spinner : BsGear} />}
|
icon={<Icon as={loading ? Spinner : BsGear} />}
|
||||||
|
maxW={8}
|
||||||
|
minW={8}
|
||||||
|
minH={8}
|
||||||
|
maxH={8}
|
||||||
/>
|
/>
|
||||||
<MenuList fontSize="md" zIndex="dropdown" mt={-3}>
|
<MenuList fontSize="md" zIndex="dropdown" mt={-1}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<Icon as={BsPlus} boxSize={6} mx="-5px" />}
|
icon={<Icon as={BsPlus} boxSize={6} mx="-5px" />}
|
||||||
onClick={() => onAddScenario(false)}
|
onClick={() => onAddScenario(false)}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
setIsChanged(false);
|
setIsChanged(false);
|
||||||
|
|
||||||
await utils.promptVariants.list.invalidate();
|
await utils.promptVariants.list.invalidate();
|
||||||
}, [checkForChanges]);
|
}, [checkForChanges, replaceVariant.mutateAsync]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (monaco) {
|
if (monaco) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState, type DragEvent } from "react";
|
import { useState, type DragEvent } from "react";
|
||||||
import { type PromptVariant } from "../OutputsTable/types";
|
import { type PromptVariant } from "../types";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { RiDraggable } from "react-icons/ri";
|
import { RiDraggable } from "react-icons/ri";
|
||||||
import { useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperimentAccess, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import { HStack, Icon, Text, GridItem, type GridItemProps } from "@chakra-ui/react"; // Changed here
|
import { HStack, Icon, Text, GridItem, type GridItemProps } from "@chakra-ui/react"; // Changed here
|
||||||
import { cellPadding, headerMinHeight } from "../constants";
|
import { cellPadding, headerMinHeight } from "../constants";
|
||||||
import AutoResizeTextArea from "../AutoResizeTextArea";
|
import AutoResizeTextArea from "../../AutoResizeTextArea";
|
||||||
import VariantHeaderMenuButton from "./VariantHeaderMenuButton";
|
import VariantHeaderMenuButton from "./VariantHeaderMenuButton";
|
||||||
|
|
||||||
export default function VariantHeader(
|
export default function VariantHeader(
|
||||||
@@ -84,6 +84,7 @@ export default function VariantHeader(
|
|||||||
>
|
>
|
||||||
<HStack
|
<HStack
|
||||||
spacing={2}
|
spacing={2}
|
||||||
|
py={2}
|
||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
minH={headerMinHeight}
|
minH={headerMinHeight}
|
||||||
draggable={!isInputHovered}
|
draggable={!isInputHovered}
|
||||||
@@ -102,7 +103,9 @@ export default function VariantHeader(
|
|||||||
setIsDragTarget(false);
|
setIsDragTarget(false);
|
||||||
}}
|
}}
|
||||||
onDrop={onReorder}
|
onDrop={onReorder}
|
||||||
backgroundColor={isDragTarget ? "gray.200" : "gray.100"}
|
backgroundColor={isDragTarget ? "gray.200" : "white"}
|
||||||
|
borderTopLeftRadius={gridItemProps.borderTopLeftRadius}
|
||||||
|
borderTopRightRadius={gridItemProps.borderTopRightRadius}
|
||||||
h="full"
|
h="full"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import { type PromptVariant } from "../OutputsTable/types";
|
import { useState } from "react";
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -14,10 +12,13 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { BsFillTrashFill, BsGear, BsStars } from "react-icons/bs";
|
import { BsFillTrashFill, BsGear, BsStars } from "react-icons/bs";
|
||||||
import { FaRegClone } from "react-icons/fa";
|
import { FaRegClone } from "react-icons/fa";
|
||||||
import { useState } from "react";
|
|
||||||
import { RefinePromptModal } from "../RefinePromptModal/RefinePromptModal";
|
|
||||||
import { RiExchangeFundsFill } from "react-icons/ri";
|
import { RiExchangeFundsFill } from "react-icons/ri";
|
||||||
import { ChangeModelModal } from "../ChangeModelModal/ChangeModelModal";
|
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
||||||
|
import { type PromptVariant } from "../types";
|
||||||
|
import { RefinePromptModal } from "../../RefinePromptModal/RefinePromptModal";
|
||||||
|
import { ChangeModelModal } from "../../ChangeModelModal/ChangeModelModal";
|
||||||
|
|
||||||
export default function VariantHeaderMenuButton({
|
export default function VariantHeaderMenuButton({
|
||||||
variant,
|
variant,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { HStack, Icon, Text, useToken } from "@chakra-ui/react";
|
import { HStack, Icon, Text, useToken } from "@chakra-ui/react";
|
||||||
import { type PromptVariant } from "./types";
|
import { type PromptVariant } from "./types";
|
||||||
import { cellPadding } from "../constants";
|
import { cellPadding } from "./constants";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import chroma from "chroma-js";
|
import chroma from "chroma-js";
|
||||||
import { BsCurrencyDollar } from "react-icons/bs";
|
import { BsCurrencyDollar } from "react-icons/bs";
|
||||||
@@ -21,14 +21,18 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
|||||||
outputTokens: 0,
|
outputTokens: 0,
|
||||||
scenarioCount: 0,
|
scenarioCount: 0,
|
||||||
outputCount: 0,
|
outputCount: 0,
|
||||||
|
awaitingCompletions: false,
|
||||||
awaitingEvals: false,
|
awaitingEvals: false,
|
||||||
},
|
},
|
||||||
refetchInterval,
|
refetchInterval,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Poll every two seconds while we are waiting for LLM retrievals to finish
|
// Poll every five seconds while we are waiting for LLM retrievals to finish
|
||||||
useEffect(() => setRefetchInterval(data.awaitingEvals ? 5000 : 0), [data.awaitingEvals]);
|
useEffect(
|
||||||
|
() => setRefetchInterval(data.awaitingCompletions || data.awaitingEvals ? 5000 : 0),
|
||||||
|
[data.awaitingCompletions, data.awaitingEvals],
|
||||||
|
);
|
||||||
|
|
||||||
const [passColor, neutralColor, failColor] = useToken("colors", [
|
const [passColor, neutralColor, failColor] = useToken("colors", [
|
||||||
"green.500",
|
"green.500",
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { api } from "~/utils/api";
|
|||||||
import AddVariantButton from "./AddVariantButton";
|
import AddVariantButton from "./AddVariantButton";
|
||||||
import ScenarioRow from "./ScenarioRow";
|
import ScenarioRow from "./ScenarioRow";
|
||||||
import VariantEditor from "./VariantEditor";
|
import VariantEditor from "./VariantEditor";
|
||||||
import VariantHeader from "../VariantHeader/VariantHeader";
|
import VariantHeader from "./VariantHeader/VariantHeader";
|
||||||
import VariantStats from "./VariantStats";
|
import VariantStats from "./VariantStats";
|
||||||
import { ScenariosHeader } from "./ScenariosHeader";
|
import { ScenariosHeader } from "./ScenariosHeader";
|
||||||
import { borders } from "./styles";
|
import { borders } from "./styles";
|
||||||
import { useScenarios } from "~/utils/hooks";
|
import { useScenarios } from "~/utils/hooks";
|
||||||
import ScenarioPaginator from "./ScenarioPaginator";
|
import ScenarioPaginator from "./ScenarioPaginator";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
import useScrolledPast from "./useHasScrolledPast";
|
||||||
|
|
||||||
export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) {
|
export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) {
|
||||||
const variants = api.promptVariants.list.useQuery(
|
const variants = api.promptVariants.list.useQuery(
|
||||||
@@ -18,6 +19,7 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
);
|
);
|
||||||
|
|
||||||
const scenarios = useScenarios();
|
const scenarios = useScenarios();
|
||||||
|
const shouldFlattenHeader = useScrolledPast(50);
|
||||||
|
|
||||||
if (!variants.data || !scenarios.data) return null;
|
if (!variants.data || !scenarios.data) return null;
|
||||||
|
|
||||||
@@ -53,20 +55,30 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
colStart: i + 2,
|
colStart: i + 2,
|
||||||
borderLeftWidth: i === 0 ? 1 : 0,
|
borderLeftWidth: i === 0 ? 1 : 0,
|
||||||
marginLeft: i === 0 ? "-1px" : 0,
|
marginLeft: i === 0 ? "-1px" : 0,
|
||||||
backgroundColor: "gray.100",
|
backgroundColor: "white",
|
||||||
};
|
};
|
||||||
|
const isFirst = i === 0;
|
||||||
|
const isLast = i === variants.data.length - 1;
|
||||||
return (
|
return (
|
||||||
<Fragment key={variant.uiId}>
|
<Fragment key={variant.uiId}>
|
||||||
<VariantHeader
|
<VariantHeader
|
||||||
variant={variant}
|
variant={variant}
|
||||||
canHide={variants.data.length > 1}
|
canHide={variants.data.length > 1}
|
||||||
rowStart={1}
|
rowStart={1}
|
||||||
|
borderTopLeftRadius={isFirst && !shouldFlattenHeader ? 8 : 0}
|
||||||
|
borderTopRightRadius={isLast && !shouldFlattenHeader ? 8 : 0}
|
||||||
{...sharedProps}
|
{...sharedProps}
|
||||||
/>
|
/>
|
||||||
<GridItem rowStart={2} {...sharedProps}>
|
<GridItem rowStart={2} {...sharedProps}>
|
||||||
<VariantEditor variant={variant} />
|
<VariantEditor variant={variant} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem rowStart={3} {...sharedProps}>
|
<GridItem
|
||||||
|
rowStart={3}
|
||||||
|
{...sharedProps}
|
||||||
|
borderBottomLeftRadius={isFirst ? 8 : 0}
|
||||||
|
borderBottomRightRadius={isLast ? 8 : 0}
|
||||||
|
boxShadow="5px 5px 15px 1px rgba(0, 0, 0, 0.1);"
|
||||||
|
>
|
||||||
<VariantStats variant={variant} />
|
<VariantStats variant={variant} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -77,7 +89,6 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
colSpan={allCols - 1}
|
colSpan={allCols - 1}
|
||||||
rowStart={variantHeaderRows + 1}
|
rowStart={variantHeaderRows + 1}
|
||||||
colStart={1}
|
colStart={1}
|
||||||
{...borders}
|
|
||||||
borderRightWidth={0}
|
borderRightWidth={0}
|
||||||
>
|
>
|
||||||
<ScenariosHeader />
|
<ScenariosHeader />
|
||||||
@@ -90,6 +101,8 @@ export default function OutputsTable({ experimentId }: { experimentId: string |
|
|||||||
scenario={scenario}
|
scenario={scenario}
|
||||||
variants={variants.data}
|
variants={variants.data}
|
||||||
canHide={visibleScenariosCount > 1}
|
canHide={visibleScenariosCount > 1}
|
||||||
|
isFirst={i === 0}
|
||||||
|
isLast={i === visibleScenariosCount - 1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<GridItem
|
<GridItem
|
||||||
|
|||||||
34
app/src/components/OutputsTable/useHasScrolledPast.tsx
Normal file
34
app/src/components/OutputsTable/useHasScrolledPast.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const useScrolledPast = (scrollThreshold: number) => {
|
||||||
|
const [hasScrolledPast, setHasScrolledPast] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = document.getElementById("output-container");
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
console.warn('Element with id "outputs-container" not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkScroll = () => {
|
||||||
|
const { scrollTop } = container;
|
||||||
|
|
||||||
|
// Check if scrollTop is greater than or equal to scrollThreshold
|
||||||
|
setHasScrolledPast(scrollTop > scrollThreshold);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkScroll();
|
||||||
|
|
||||||
|
container.addEventListener("scroll", checkScroll);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener("scroll", checkScroll);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return hasScrolledPast;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useScrolledPast;
|
||||||
@@ -1,77 +1,126 @@
|
|||||||
import { Box, HStack, IconButton } from "@chakra-ui/react";
|
|
||||||
import {
|
import {
|
||||||
BsChevronDoubleLeft,
|
HStack,
|
||||||
BsChevronDoubleRight,
|
IconButton,
|
||||||
BsChevronLeft,
|
Text,
|
||||||
BsChevronRight,
|
Select,
|
||||||
} from "react-icons/bs";
|
type StackProps,
|
||||||
import { usePage } from "~/utils/hooks";
|
Icon,
|
||||||
|
useBreakpointValue,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { FiChevronsLeft, FiChevronsRight, FiChevronLeft, FiChevronRight } from "react-icons/fi";
|
||||||
|
import { usePageParams } from "~/utils/hooks";
|
||||||
|
|
||||||
const Paginator = ({
|
const pageSizeOptions = [10, 25, 50, 100];
|
||||||
numItemsLoaded,
|
|
||||||
startIndex,
|
const Paginator = ({ count, ...props }: { count: number; condense?: boolean } & StackProps) => {
|
||||||
lastPage,
|
const { page, pageSize, setPageParams } = usePageParams();
|
||||||
count,
|
|
||||||
}: {
|
const lastPage = Math.ceil(count / pageSize);
|
||||||
numItemsLoaded: number;
|
|
||||||
startIndex: number;
|
const updatePageSize = useCallback(
|
||||||
lastPage: number;
|
(newPageSize: number) => {
|
||||||
count: number;
|
const newPage = Math.floor(((page - 1) * pageSize) / newPageSize) + 1;
|
||||||
}) => {
|
setPageParams({ page: newPage, pageSize: newPageSize }, "replace");
|
||||||
const [page, setPage] = usePage();
|
},
|
||||||
|
[page, pageSize, setPageParams],
|
||||||
|
);
|
||||||
|
|
||||||
const nextPage = () => {
|
const nextPage = () => {
|
||||||
if (page < lastPage) {
|
if (page < lastPage) {
|
||||||
setPage(page + 1, "replace");
|
setPageParams({ page: page + 1 }, "replace");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevPage = () => {
|
const prevPage = () => {
|
||||||
if (page > 1) {
|
if (page > 1) {
|
||||||
setPage(page - 1, "replace");
|
setPageParams({ page: page - 1 }, "replace");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToLastPage = () => setPage(lastPage, "replace");
|
const goToLastPage = () => setPageParams({ page: lastPage }, "replace");
|
||||||
const goToFirstPage = () => setPage(1, "replace");
|
const goToFirstPage = () => setPageParams({ page: 1 }, "replace");
|
||||||
|
|
||||||
|
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||||
|
const condense = isMobile || props.condense;
|
||||||
|
|
||||||
|
if (count === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack pt={4}>
|
<HStack
|
||||||
<IconButton
|
pt={4}
|
||||||
variant="ghost"
|
spacing={8}
|
||||||
size="sm"
|
justifyContent={condense ? "flex-start" : "space-between"}
|
||||||
onClick={goToFirstPage}
|
alignItems="center"
|
||||||
isDisabled={page === 1}
|
w="full"
|
||||||
aria-label="Go to first page"
|
{...props}
|
||||||
icon={<BsChevronDoubleLeft />}
|
>
|
||||||
/>
|
{!condense && (
|
||||||
<IconButton
|
<>
|
||||||
variant="ghost"
|
<HStack>
|
||||||
size="sm"
|
<Text>Rows</Text>
|
||||||
onClick={prevPage}
|
<Select
|
||||||
isDisabled={page === 1}
|
value={pageSize}
|
||||||
aria-label="Previous page"
|
onChange={(e) => updatePageSize(parseInt(e.target.value))}
|
||||||
icon={<BsChevronLeft />}
|
w={20}
|
||||||
/>
|
backgroundColor="white"
|
||||||
<Box>
|
>
|
||||||
{startIndex}-{startIndex + numItemsLoaded - 1} / {count}
|
{pageSizeOptions.map((option) => (
|
||||||
</Box>
|
<option key={option} value={option}>
|
||||||
<IconButton
|
{option}
|
||||||
variant="ghost"
|
</option>
|
||||||
size="sm"
|
))}
|
||||||
onClick={nextPage}
|
</Select>
|
||||||
isDisabled={page === lastPage}
|
</HStack>
|
||||||
aria-label="Next page"
|
<Text>
|
||||||
icon={<BsChevronRight />}
|
Page {page} of {lastPage}
|
||||||
/>
|
</Text>
|
||||||
<IconButton
|
</>
|
||||||
variant="ghost"
|
)}
|
||||||
size="sm"
|
|
||||||
onClick={goToLastPage}
|
<HStack>
|
||||||
isDisabled={page === lastPage}
|
<IconButton
|
||||||
aria-label="Go to last page"
|
variant="outline"
|
||||||
icon={<BsChevronDoubleRight />}
|
size="sm"
|
||||||
/>
|
onClick={goToFirstPage}
|
||||||
|
isDisabled={page === 1}
|
||||||
|
aria-label="Go to first page"
|
||||||
|
icon={<Icon as={FiChevronsLeft} boxSize={5} strokeWidth={1.5} />}
|
||||||
|
bgColor="white"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={prevPage}
|
||||||
|
isDisabled={page === 1}
|
||||||
|
aria-label="Previous page"
|
||||||
|
icon={<Icon as={FiChevronLeft} boxSize={5} strokeWidth={1.5} />}
|
||||||
|
bgColor="white"
|
||||||
|
/>
|
||||||
|
{condense && (
|
||||||
|
<Text>
|
||||||
|
Page {page} of {lastPage}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={nextPage}
|
||||||
|
isDisabled={page === lastPage}
|
||||||
|
aria-label="Next page"
|
||||||
|
icon={<Icon as={FiChevronRight} boxSize={5} strokeWidth={1.5} />}
|
||||||
|
bgColor="white"
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={goToLastPage}
|
||||||
|
isDisabled={page === lastPage}
|
||||||
|
aria-label="Go to last page"
|
||||||
|
icon={<Icon as={FiChevronsRight} boxSize={5} strokeWidth={1.5} />}
|
||||||
|
bgColor="white"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
Heading,
|
|
||||||
Table,
|
|
||||||
Tbody,
|
|
||||||
Td,
|
|
||||||
Th,
|
|
||||||
Thead,
|
|
||||||
Tr,
|
|
||||||
Tooltip,
|
|
||||||
Collapse,
|
|
||||||
HStack,
|
|
||||||
VStack,
|
|
||||||
IconButton,
|
|
||||||
useToast,
|
|
||||||
Icon,
|
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
|
||||||
import { ChevronUpIcon, ChevronDownIcon, CopyIcon } from "lucide-react";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import { type RouterOutputs, api } from "~/utils/api";
|
|
||||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
|
||||||
import { atelierCaveLight } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
|
||||||
import stringify from "json-stringify-pretty-compact";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
|
||||||
|
|
||||||
type LoggedCall = RouterOutputs["dashboard"]["loggedCalls"][0];
|
|
||||||
|
|
||||||
const FormattedJson = ({ json }: { json: any }) => {
|
|
||||||
const jsonString = stringify(json, { maxLength: 40 });
|
|
||||||
const toast = useToast();
|
|
||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text);
|
|
||||||
toast({
|
|
||||||
title: "Copied to clipboard",
|
|
||||||
status: "success",
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: "Failed to copy to clipboard",
|
|
||||||
status: "error",
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box position="relative" fontSize="sm" borderRadius="md" overflow="hidden">
|
|
||||||
<SyntaxHighlighter
|
|
||||||
customStyle={{ overflowX: "unset" }}
|
|
||||||
language="json"
|
|
||||||
style={atelierCaveLight}
|
|
||||||
lineProps={{
|
|
||||||
style: { wordBreak: "break-all", whiteSpace: "pre-wrap" },
|
|
||||||
}}
|
|
||||||
wrapLines
|
|
||||||
>
|
|
||||||
{jsonString}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
<IconButton
|
|
||||||
aria-label="Copy"
|
|
||||||
icon={<CopyIcon />}
|
|
||||||
position="absolute"
|
|
||||||
top={1}
|
|
||||||
right={1}
|
|
||||||
size="xs"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => void copyToClipboard(jsonString)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function TableRow({
|
|
||||||
loggedCall,
|
|
||||||
isExpanded,
|
|
||||||
onToggle,
|
|
||||||
}: {
|
|
||||||
loggedCall: LoggedCall;
|
|
||||||
isExpanded: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
}) {
|
|
||||||
const isError = loggedCall.modelResponse?.statusCode !== 200;
|
|
||||||
const timeAgo = dayjs(loggedCall.requestedAt).fromNow();
|
|
||||||
const fullTime = dayjs(loggedCall.requestedAt).toString();
|
|
||||||
|
|
||||||
const model = useMemo(
|
|
||||||
() => loggedCall.tags.find((tag) => tag.name.startsWith("$model"))?.value,
|
|
||||||
[loggedCall.tags],
|
|
||||||
);
|
|
||||||
|
|
||||||
const durationCell = (
|
|
||||||
<Td isNumeric>
|
|
||||||
{loggedCall.cacheHit
|
|
||||||
? "Cache hit"
|
|
||||||
: ((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s"}
|
|
||||||
</Td>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tr
|
|
||||||
onClick={onToggle}
|
|
||||||
key={loggedCall.id}
|
|
||||||
_hover={{ bgColor: "gray.100", cursor: "pointer" }}
|
|
||||||
sx={{
|
|
||||||
"> td": { borderBottom: "none" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Td>
|
|
||||||
<Icon boxSize={6} as={isExpanded ? ChevronUpIcon : ChevronDownIcon} />
|
|
||||||
</Td>
|
|
||||||
<Td>
|
|
||||||
<Tooltip label={fullTime} placement="top">
|
|
||||||
<Box whiteSpace="nowrap" minW="120px">
|
|
||||||
{timeAgo}
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
</Td>
|
|
||||||
<Td width="100%">{model}</Td>
|
|
||||||
{durationCell}
|
|
||||||
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
|
|
||||||
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
|
|
||||||
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
|
|
||||||
{loggedCall.modelResponse?.statusCode ?? "No response"}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={8} p={0}>
|
|
||||||
<Collapse in={isExpanded} unmountOnExit={true}>
|
|
||||||
<VStack p={4} align="stretch">
|
|
||||||
<HStack align="stretch">
|
|
||||||
<VStack flex={1} align="stretch">
|
|
||||||
<Heading size="sm">Input</Heading>
|
|
||||||
<FormattedJson json={loggedCall.modelResponse?.reqPayload} />
|
|
||||||
</VStack>
|
|
||||||
<VStack flex={1} align="stretch">
|
|
||||||
<Heading size="sm">Output</Heading>
|
|
||||||
<FormattedJson json={loggedCall.modelResponse?.respPayload} />
|
|
||||||
</VStack>
|
|
||||||
</HStack>
|
|
||||||
<ButtonGroup alignSelf="flex-end">
|
|
||||||
<Button as={Link} colorScheme="blue" href={{ pathname: "/experiments" }}>
|
|
||||||
Experiments
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</VStack>
|
|
||||||
</Collapse>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LoggedCallTable() {
|
|
||||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
|
||||||
const loggedCalls = api.dashboard.loggedCalls.useQuery({});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card variant="outline" width="100%" overflow="hidden">
|
|
||||||
<CardHeader>
|
|
||||||
<Heading as="h3" size="sm">
|
|
||||||
Logged Calls
|
|
||||||
</Heading>
|
|
||||||
</CardHeader>
|
|
||||||
<Table>
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th />
|
|
||||||
<Th>Time</Th>
|
|
||||||
<Th>Model</Th>
|
|
||||||
<Th isNumeric>Duration</Th>
|
|
||||||
<Th isNumeric>Input tokens</Th>
|
|
||||||
<Th isNumeric>Output tokens</Th>
|
|
||||||
<Th isNumeric>Status</Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{loggedCalls.data?.map((loggedCall) => {
|
|
||||||
return (
|
|
||||||
<TableRow
|
|
||||||
key={loggedCall.id}
|
|
||||||
loggedCall={loggedCall}
|
|
||||||
isExpanded={loggedCall.id === expandedRow}
|
|
||||||
onToggle={() => {
|
|
||||||
if (loggedCall.id === expandedRow) {
|
|
||||||
setExpandedRow(null);
|
|
||||||
} else {
|
|
||||||
setExpandedRow(loggedCall.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
46
app/src/components/dashboard/LoggedCallsTable.tsx
Normal file
46
app/src/components/dashboard/LoggedCallsTable.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Card, CardHeader, Heading, Table, Tbody, HStack, Button, Text } from "@chakra-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useLoggedCalls } from "~/utils/hooks";
|
||||||
|
import { TableHeader, TableRow } from "../requestLogs/TableRow";
|
||||||
|
|
||||||
|
export default function LoggedCallsTable() {
|
||||||
|
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||||
|
const { data: loggedCalls } = useLoggedCalls();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card width="100%" overflow="hidden">
|
||||||
|
<CardHeader>
|
||||||
|
<HStack justifyContent="space-between">
|
||||||
|
<Heading as="h3" size="sm">
|
||||||
|
Request Logs
|
||||||
|
</Heading>
|
||||||
|
<Button as={Link} href="/request-logs" variant="ghost" colorScheme="blue">
|
||||||
|
<Text>View All</Text>
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
<Table>
|
||||||
|
<TableHeader />
|
||||||
|
<Tbody>
|
||||||
|
{loggedCalls?.calls.map((loggedCall) => {
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={loggedCall.id}
|
||||||
|
loggedCall={loggedCall}
|
||||||
|
isExpanded={loggedCall.id === expandedRow}
|
||||||
|
onToggle={() => {
|
||||||
|
if (loggedCall.id === expandedRow) {
|
||||||
|
setExpandedRow(null);
|
||||||
|
} else {
|
||||||
|
setExpandedRow(loggedCall.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import {
|
|
||||||
HStack,
|
|
||||||
Icon,
|
|
||||||
VStack,
|
|
||||||
Text,
|
|
||||||
Divider,
|
|
||||||
Spinner,
|
|
||||||
AspectRatio,
|
|
||||||
SkeletonText,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { RiDatabase2Line } from "react-icons/ri";
|
|
||||||
import { formatTimePast } from "~/utils/dayjs";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { BsPlusSquare } from "react-icons/bs";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
|
|
||||||
type DatasetData = {
|
|
||||||
name: string;
|
|
||||||
numEntries: number;
|
|
||||||
id: string;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DatasetCard = ({ dataset }: { dataset: DatasetData }) => {
|
|
||||||
return (
|
|
||||||
<AspectRatio ratio={1.2} w="full">
|
|
||||||
<VStack
|
|
||||||
as={Link}
|
|
||||||
href={{ pathname: "/data/[id]", query: { id: dataset.id } }}
|
|
||||||
bg="gray.50"
|
|
||||||
_hover={{ bg: "gray.100" }}
|
|
||||||
transition="background 0.2s"
|
|
||||||
cursor="pointer"
|
|
||||||
borderColor="gray.200"
|
|
||||||
borderWidth={1}
|
|
||||||
p={4}
|
|
||||||
justify="space-between"
|
|
||||||
>
|
|
||||||
<HStack w="full" color="gray.700" justify="center">
|
|
||||||
<Icon as={RiDatabase2Line} boxSize={4} />
|
|
||||||
<Text fontWeight="bold">{dataset.name}</Text>
|
|
||||||
</HStack>
|
|
||||||
<HStack h="full" spacing={4} flex={1} align="center">
|
|
||||||
<CountLabel label="Rows" count={dataset.numEntries} />
|
|
||||||
</HStack>
|
|
||||||
<HStack w="full" color="gray.500" fontSize="xs" textAlign="center">
|
|
||||||
<Text flex={1}>Created {formatTimePast(dataset.createdAt)}</Text>
|
|
||||||
<Divider h={4} orientation="vertical" />
|
|
||||||
<Text flex={1}>Updated {formatTimePast(dataset.updatedAt)}</Text>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
</AspectRatio>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CountLabel = ({ label, count }: { label: string; count: number }) => {
|
|
||||||
return (
|
|
||||||
<VStack alignItems="center" flex={1}>
|
|
||||||
<Text color="gray.500" fontWeight="bold">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" color="gray.500">
|
|
||||||
{count}
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NewDatasetCard = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
|
||||||
const createMutation = api.datasets.create.useMutation();
|
|
||||||
const [createDataset, isLoading] = useHandledAsyncCallback(async () => {
|
|
||||||
const newDataset = await createMutation.mutateAsync({ projectId: selectedProjectId ?? "" });
|
|
||||||
await router.push({ pathname: "/data/[id]", query: { id: newDataset.id } });
|
|
||||||
}, [createMutation, router, selectedProjectId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AspectRatio ratio={1.2} w="full">
|
|
||||||
<VStack
|
|
||||||
align="center"
|
|
||||||
justify="center"
|
|
||||||
_hover={{ cursor: "pointer", bg: "gray.50" }}
|
|
||||||
transition="background 0.2s"
|
|
||||||
cursor="pointer"
|
|
||||||
borderColor="gray.200"
|
|
||||||
borderWidth={1}
|
|
||||||
p={4}
|
|
||||||
onClick={createDataset}
|
|
||||||
>
|
|
||||||
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} />
|
|
||||||
<Text display={{ base: "none", md: "block" }} ml={2}>
|
|
||||||
New Dataset
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</AspectRatio>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DatasetCardSkeleton = () => (
|
|
||||||
<AspectRatio ratio={1.2} w="full">
|
|
||||||
<VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="gray.50">
|
|
||||||
<SkeletonText noOfLines={1} w="80%" />
|
|
||||||
<SkeletonText noOfLines={2} w="60%" />
|
|
||||||
<SkeletonText noOfLines={1} w="80%" />
|
|
||||||
</VStack>
|
|
||||||
</AspectRatio>
|
|
||||||
);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { useDatasetEntries } from "~/utils/hooks";
|
|
||||||
import Paginator from "../Paginator";
|
|
||||||
|
|
||||||
const DatasetEntriesPaginator = () => {
|
|
||||||
const { data } = useDatasetEntries();
|
|
||||||
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
const { entries, startIndex, lastPage, count } = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paginator
|
|
||||||
numItemsLoaded={entries.length}
|
|
||||||
startIndex={startIndex}
|
|
||||||
lastPage={lastPage}
|
|
||||||
count={count}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DatasetEntriesPaginator;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { type StackProps, VStack, Table, Th, Tr, Thead, Tbody, Text } from "@chakra-ui/react";
|
|
||||||
import { useDatasetEntries } from "~/utils/hooks";
|
|
||||||
import TableRow from "./TableRow";
|
|
||||||
import DatasetEntriesPaginator from "./DatasetEntriesPaginator";
|
|
||||||
|
|
||||||
const DatasetEntriesTable = (props: StackProps) => {
|
|
||||||
const { data } = useDatasetEntries();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack justifyContent="space-between" {...props}>
|
|
||||||
<Table variant="simple" sx={{ "table-layout": "fixed", width: "full" }}>
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th>Input</Th>
|
|
||||||
<Th>Output</Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>{data?.entries.map((entry) => <TableRow key={entry.id} entry={entry} />)}</Tbody>
|
|
||||||
</Table>
|
|
||||||
{(!data || data.entries.length) === 0 ? (
|
|
||||||
<Text alignSelf="flex-start" pl={6} color="gray.500">
|
|
||||||
No entries found
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<DatasetEntriesPaginator />
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DatasetEntriesTable;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { Button, HStack, useDisclosure } from "@chakra-ui/react";
|
|
||||||
import { BiImport } from "react-icons/bi";
|
|
||||||
import { BsStars } from "react-icons/bs";
|
|
||||||
|
|
||||||
import { GenerateDataModal } from "./GenerateDataModal";
|
|
||||||
|
|
||||||
export const DatasetHeaderButtons = () => {
|
|
||||||
const generateModalDisclosure = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<HStack>
|
|
||||||
<Button leftIcon={<BiImport />} colorScheme="blue" variant="ghost">
|
|
||||||
Import Data
|
|
||||||
</Button>
|
|
||||||
<Button leftIcon={<BsStars />} colorScheme="blue" onClick={generateModalDisclosure.onOpen}>
|
|
||||||
Generate Data
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
<GenerateDataModal
|
|
||||||
isOpen={generateModalDisclosure.isOpen}
|
|
||||||
onClose={generateModalDisclosure.onClose}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalFooter,
|
|
||||||
Text,
|
|
||||||
HStack,
|
|
||||||
VStack,
|
|
||||||
Icon,
|
|
||||||
NumberInput,
|
|
||||||
NumberInputField,
|
|
||||||
NumberInputStepper,
|
|
||||||
NumberIncrementStepper,
|
|
||||||
NumberDecrementStepper,
|
|
||||||
Button,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { BsStars } from "react-icons/bs";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useDataset, useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import AutoResizeTextArea from "~/components/AutoResizeTextArea";
|
|
||||||
|
|
||||||
export const GenerateDataModal = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
}: {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}) => {
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const datasetId = useDataset().data?.id;
|
|
||||||
|
|
||||||
const [numToGenerate, setNumToGenerate] = useState<number>(20);
|
|
||||||
const [inputDescription, setInputDescription] = useState<string>(
|
|
||||||
"Each input should contain an email body. Half of the emails should contain event details, and the other half should not.",
|
|
||||||
);
|
|
||||||
const [outputDescription, setOutputDescription] = useState<string>(
|
|
||||||
`Each output should contain "true" or "false", where "true" indicates that the email contains event details.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const generateEntriesMutation = api.datasetEntries.autogenerateEntries.useMutation();
|
|
||||||
|
|
||||||
const [generateEntries, generateEntriesInProgress] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!inputDescription || !outputDescription || !numToGenerate || !datasetId) return;
|
|
||||||
await generateEntriesMutation.mutateAsync({
|
|
||||||
datasetId,
|
|
||||||
inputDescription,
|
|
||||||
outputDescription,
|
|
||||||
numToGenerate,
|
|
||||||
});
|
|
||||||
await utils.datasetEntries.list.invalidate();
|
|
||||||
onClose();
|
|
||||||
}, [
|
|
||||||
generateEntriesMutation,
|
|
||||||
onClose,
|
|
||||||
inputDescription,
|
|
||||||
outputDescription,
|
|
||||||
numToGenerate,
|
|
||||||
datasetId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose} size={{ base: "xl", sm: "2xl", md: "3xl" }}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent w={1200}>
|
|
||||||
<ModalHeader>
|
|
||||||
<HStack>
|
|
||||||
<Icon as={BsStars} />
|
|
||||||
<Text>Generate Data</Text>
|
|
||||||
</HStack>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody maxW="unset">
|
|
||||||
<VStack w="full" spacing={8} padding={8} alignItems="flex-start">
|
|
||||||
<VStack alignItems="flex-start" spacing={2}>
|
|
||||||
<Text fontWeight="bold">Number of Rows:</Text>
|
|
||||||
<NumberInput
|
|
||||||
step={5}
|
|
||||||
defaultValue={15}
|
|
||||||
min={0}
|
|
||||||
max={100}
|
|
||||||
onChange={(valueString) => setNumToGenerate(parseInt(valueString) || 0)}
|
|
||||||
value={numToGenerate}
|
|
||||||
w="24"
|
|
||||||
>
|
|
||||||
<NumberInputField />
|
|
||||||
<NumberInputStepper>
|
|
||||||
<NumberIncrementStepper />
|
|
||||||
<NumberDecrementStepper />
|
|
||||||
</NumberInputStepper>
|
|
||||||
</NumberInput>
|
|
||||||
</VStack>
|
|
||||||
<VStack alignItems="flex-start" w="full" spacing={2}>
|
|
||||||
<Text fontWeight="bold">Input Description:</Text>
|
|
||||||
<AutoResizeTextArea
|
|
||||||
value={inputDescription}
|
|
||||||
onChange={(e) => setInputDescription(e.target.value)}
|
|
||||||
placeholder="Each input should contain..."
|
|
||||||
/>
|
|
||||||
</VStack>
|
|
||||||
<VStack alignItems="flex-start" w="full" spacing={2}>
|
|
||||||
<Text fontWeight="bold">Output Description (optional):</Text>
|
|
||||||
<AutoResizeTextArea
|
|
||||||
value={outputDescription}
|
|
||||||
onChange={(e) => setOutputDescription(e.target.value)}
|
|
||||||
placeholder="The output should contain..."
|
|
||||||
/>
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
isLoading={generateEntriesInProgress}
|
|
||||||
isDisabled={!numToGenerate || !inputDescription || !outputDescription}
|
|
||||||
onClick={generateEntries}
|
|
||||||
>
|
|
||||||
Generate
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Td, Tr } from "@chakra-ui/react";
|
|
||||||
import { type DatasetEntry } from "@prisma/client";
|
|
||||||
|
|
||||||
const TableRow = ({ entry }: { entry: DatasetEntry }) => {
|
|
||||||
return (
|
|
||||||
<Tr key={entry.id}>
|
|
||||||
<Td>{entry.input}</Td>
|
|
||||||
<Td>{entry.output}</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TableRow;
|
|
||||||
@@ -7,39 +7,35 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
AspectRatio,
|
AspectRatio,
|
||||||
SkeletonText,
|
SkeletonText,
|
||||||
|
Card,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { RiFlaskLine } from "react-icons/ri";
|
import { RiFlaskLine } from "react-icons/ri";
|
||||||
import { formatTimePast } from "~/utils/dayjs";
|
import { formatTimePast } from "~/utils/dayjs";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { BsPlusSquare } from "react-icons/bs";
|
import { BsPlusSquare } from "react-icons/bs";
|
||||||
import { api } from "~/utils/api";
|
import { RouterOutputs, api } from "~/utils/api";
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
|
|
||||||
type ExperimentData = {
|
export const ExperimentCard = ({ exp }: { exp: RouterOutputs["experiments"]["list"][0] }) => {
|
||||||
testScenarioCount: number;
|
|
||||||
promptVariantCount: number;
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
sortIndex: number;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
|
|
||||||
return (
|
return (
|
||||||
<AspectRatio ratio={1.2} w="full">
|
<Card
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
cursor="pointer"
|
||||||
|
p={4}
|
||||||
|
bg="white"
|
||||||
|
borderRadius={4}
|
||||||
|
_hover={{ bg: "gray.100" }}
|
||||||
|
transition="background 0.2s"
|
||||||
|
aspectRatio={1.2}
|
||||||
|
>
|
||||||
<VStack
|
<VStack
|
||||||
as={Link}
|
as={Link}
|
||||||
href={{ pathname: "/experiments/[id]", query: { id: exp.id } }}
|
w="full"
|
||||||
bg="gray.50"
|
h="full"
|
||||||
_hover={{ bg: "gray.100" }}
|
href={{ pathname: "/experiments/[experimentSlug]", query: { experimentSlug: exp.slug } }}
|
||||||
transition="background 0.2s"
|
|
||||||
cursor="pointer"
|
|
||||||
borderColor="gray.200"
|
|
||||||
borderWidth={1}
|
|
||||||
p={4}
|
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
>
|
>
|
||||||
<HStack w="full" color="gray.700" justify="center">
|
<HStack w="full" color="gray.700" justify="center">
|
||||||
@@ -57,7 +53,7 @@ export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
|
|||||||
<Text flex={1}>Updated {formatTimePast(exp.updatedAt)}</Text>
|
<Text flex={1}>Updated {formatTimePast(exp.updatedAt)}</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
</AspectRatio>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,36 +79,36 @@ export const NewExperimentCard = () => {
|
|||||||
projectId: selectedProjectId ?? "",
|
projectId: selectedProjectId ?? "",
|
||||||
});
|
});
|
||||||
await router.push({
|
await router.push({
|
||||||
pathname: "/experiments/[id]",
|
pathname: "/experiments/[experimentSlug]",
|
||||||
query: { id: newExperiment.id },
|
query: { experimentSlug: newExperiment.slug },
|
||||||
});
|
});
|
||||||
}, [createMutation, router, selectedProjectId]);
|
}, [createMutation, router, selectedProjectId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AspectRatio ratio={1.2} w="full">
|
<Card
|
||||||
<VStack
|
w="full"
|
||||||
align="center"
|
h="full"
|
||||||
justify="center"
|
cursor="pointer"
|
||||||
_hover={{ cursor: "pointer", bg: "gray.50" }}
|
p={4}
|
||||||
transition="background 0.2s"
|
bg="white"
|
||||||
cursor="pointer"
|
borderRadius={4}
|
||||||
borderColor="gray.200"
|
_hover={{ bg: "gray.100" }}
|
||||||
borderWidth={1}
|
transition="background 0.2s"
|
||||||
p={4}
|
aspectRatio={1.2}
|
||||||
onClick={createExperiment}
|
>
|
||||||
>
|
<VStack align="center" justify="center" w="full" h="full" p={4} onClick={createExperiment}>
|
||||||
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} />
|
<Icon as={isLoading ? Spinner : BsPlusSquare} boxSize={8} />
|
||||||
<Text display={{ base: "none", md: "block" }} ml={2}>
|
<Text display={{ base: "none", md: "block" }} ml={2}>
|
||||||
New Experiment
|
New Experiment
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</AspectRatio>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ExperimentCardSkeleton = () => (
|
export const ExperimentCardSkeleton = () => (
|
||||||
<AspectRatio ratio={1.2} w="full">
|
<AspectRatio ratio={1.2} w="full">
|
||||||
<VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="gray.50">
|
<VStack align="center" borderColor="gray.200" borderWidth={1} p={4} bg="white">
|
||||||
<SkeletonText noOfLines={1} w="80%" />
|
<SkeletonText noOfLines={1} w="80%" />
|
||||||
<SkeletonText noOfLines={2} w="60%" />
|
<SkeletonText noOfLines={2} w="60%" />
|
||||||
<SkeletonText noOfLines={1} w="80%" />
|
<SkeletonText noOfLines={1} w="80%" />
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ export const useOnForkButtonPressed = () => {
|
|||||||
|
|
||||||
const [onFork, isForking] = useHandledAsyncCallback(async () => {
|
const [onFork, isForking] = useHandledAsyncCallback(async () => {
|
||||||
if (!experiment.data?.id || !selectedProjectId) return;
|
if (!experiment.data?.id || !selectedProjectId) return;
|
||||||
const forkedExperimentId = await forkMutation.mutateAsync({
|
const newExperiment = await forkMutation.mutateAsync({
|
||||||
id: experiment.data.id,
|
id: experiment.data.id,
|
||||||
projectId: selectedProjectId,
|
projectId: selectedProjectId,
|
||||||
});
|
});
|
||||||
await router.push({ pathname: "/experiments/[id]", query: { id: forkedExperimentId } });
|
await router.push({
|
||||||
|
pathname: "/experiments/[experimentSlug]",
|
||||||
|
query: { experimentSlug: newExperiment.slug },
|
||||||
|
});
|
||||||
}, [forkMutation, experiment.data?.id, router]);
|
}, [forkMutation, experiment.data?.id, router]);
|
||||||
|
|
||||||
const onForkButtonPressed = useCallback(() => {
|
const onForkButtonPressed = useCallback(() => {
|
||||||
|
|||||||
65
app/src/components/fineTunes/FineTunesTable.tsx
Normal file
65
app/src/components/fineTunes/FineTunesTable.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Card, Table, Thead, Tr, Th, Tbody, Td, VStack, Icon, Text } from "@chakra-ui/react";
|
||||||
|
import { FaTable } from "react-icons/fa";
|
||||||
|
import { type FineTuneStatus } from "@prisma/client";
|
||||||
|
|
||||||
|
import dayjs from "~/utils/dayjs";
|
||||||
|
import { useFineTunes } from "~/utils/hooks";
|
||||||
|
|
||||||
|
const FineTunesTable = ({}) => {
|
||||||
|
const { data } = useFineTunes();
|
||||||
|
|
||||||
|
const fineTunes = data?.fineTunes || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card width="100%" overflowX="auto">
|
||||||
|
{fineTunes.length ? (
|
||||||
|
<Table>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>ID</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
|
<Th>Base Model</Th>
|
||||||
|
<Th>Dataset Size</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{fineTunes.map((fineTune) => {
|
||||||
|
return (
|
||||||
|
<Tr key={fineTune.id}>
|
||||||
|
<Td>{fineTune.slug}</Td>
|
||||||
|
<Td>{dayjs(fineTune.createdAt).format("MMMM D h:mm A")}</Td>
|
||||||
|
<Td>{fineTune.baseModel}</Td>
|
||||||
|
<Td>{fineTune.dataset._count.datasetEntries}</Td>
|
||||||
|
<Td fontSize="sm" fontWeight="bold">
|
||||||
|
<Text color={getStatusColor(fineTune.status)}>{fineTune.status}</Text>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<VStack py={8}>
|
||||||
|
<Icon as={FaTable} boxSize={16} color="gray.300" />
|
||||||
|
<Text color="gray.400" fontSize="lg" fontWeight="bold">
|
||||||
|
No Fine Tunes Found
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FineTunesTable;
|
||||||
|
|
||||||
|
const getStatusColor = (status: FineTuneStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case "DEPLOYED":
|
||||||
|
return "green.500";
|
||||||
|
case "ERROR":
|
||||||
|
return "red.500";
|
||||||
|
default:
|
||||||
|
return "yellow.500";
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
Heading,
|
Heading,
|
||||||
VStack,
|
VStack,
|
||||||
@@ -9,28 +9,36 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Link as ChakraLink,
|
Link as ChakraLink,
|
||||||
Flex,
|
Flex,
|
||||||
|
useBreakpointValue,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs";
|
import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs";
|
||||||
import { IoStatsChartOutline } from "react-icons/io5";
|
import { IoStatsChartOutline } from "react-icons/io5";
|
||||||
import { RiDatabase2Line, RiFlaskLine } from "react-icons/ri";
|
import { RiHome3Line, RiFlaskLine } from "react-icons/ri";
|
||||||
|
import { FaRobot } from "react-icons/fa";
|
||||||
import { signIn, useSession } from "next-auth/react";
|
import { signIn, useSession } from "next-auth/react";
|
||||||
import UserMenu from "./UserMenu";
|
|
||||||
import { env } from "~/env.mjs";
|
|
||||||
import ProjectMenu from "./ProjectMenu";
|
import ProjectMenu from "./ProjectMenu";
|
||||||
import NavSidebarOption from "./NavSidebarOption";
|
import NavSidebarOption from "./NavSidebarOption";
|
||||||
import IconLink from "./IconLink";
|
import IconLink from "./IconLink";
|
||||||
|
import { BetaModal } from "./BetaModal";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
|
||||||
const Divider = () => <Box h="1px" bgColor="gray.300" w="full" />;
|
const Divider = () => <Box h="1px" bgColor="gray.300" w="full" />;
|
||||||
|
|
||||||
const NavSidebar = () => {
|
const NavSidebar = () => {
|
||||||
const user = useSession().data;
|
const user = useSession().data;
|
||||||
|
|
||||||
|
// Hack to get around initial flash, see https://github.com/chakra-ui/chakra-ui/issues/6452
|
||||||
|
const isMobile = useBreakpointValue({ base: true, md: false, ssr: false });
|
||||||
|
const renderCount = useRef(0);
|
||||||
|
renderCount.current++;
|
||||||
|
|
||||||
|
const displayLogo = isMobile && renderCount.current > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack
|
<VStack
|
||||||
align="stretch"
|
align="stretch"
|
||||||
bgColor="gray.50"
|
|
||||||
py={2}
|
py={2}
|
||||||
px={2}
|
px={2}
|
||||||
pb={0}
|
pb={0}
|
||||||
@@ -40,39 +48,55 @@ const NavSidebar = () => {
|
|||||||
borderRightWidth={1}
|
borderRightWidth={1}
|
||||||
borderColor="gray.300"
|
borderColor="gray.300"
|
||||||
>
|
>
|
||||||
<HStack
|
{displayLogo && (
|
||||||
as={Link}
|
<>
|
||||||
href="/"
|
<HStack
|
||||||
_hover={{ textDecoration: "none" }}
|
as={Link}
|
||||||
spacing={{ base: 1, md: 0 }}
|
href="/"
|
||||||
mx={2}
|
_hover={{ textDecoration: "none" }}
|
||||||
py={{ base: 1, md: 2 }}
|
spacing={{ base: 1, md: 0 }}
|
||||||
>
|
mx={2}
|
||||||
<Image src="/logo.svg" alt="" boxSize={6} mr={4} ml={{ base: 0.5, md: 0 }} />
|
py={{ base: 1, md: 2 }}
|
||||||
<Heading size="md" fontFamily="inconsolata, monospace">
|
>
|
||||||
OpenPipe
|
<Image src="/logo.svg" alt="" boxSize={6} mr={4} ml={{ base: 0.5, md: 0 }} />
|
||||||
</Heading>
|
<Heading size="md" fontFamily="inconsolata, monospace">
|
||||||
</HStack>
|
OpenPipe
|
||||||
<Divider />
|
</Heading>
|
||||||
|
</HStack>
|
||||||
|
<Divider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<VStack align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
|
<VStack align="flex-start" overflowY="auto" overflowX="hidden" flex={1}>
|
||||||
{user != null && (
|
{user != null && (
|
||||||
<>
|
<>
|
||||||
<ProjectMenu />
|
<ProjectMenu />
|
||||||
<Divider />
|
<Divider />
|
||||||
{env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS && (
|
|
||||||
<IconLink icon={IoStatsChartOutline} label="Logged Calls" href="/logged-calls" beta />
|
<IconLink icon={RiHome3Line} label="Dashboard" href="/dashboard" beta />
|
||||||
)}
|
<IconLink icon={IoStatsChartOutline} label="Request Logs" href="/request-logs" beta />
|
||||||
|
<IconLink icon={FaRobot} label="Fine Tunes" href="/fine-tunes" beta />
|
||||||
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
||||||
{env.NEXT_PUBLIC_SHOW_DATA && (
|
<VStack w="full" alignItems="flex-start" spacing={0} pt={8}>
|
||||||
<IconLink icon={RiDatabase2Line} label="Data" href="/data" />
|
<Text
|
||||||
)}
|
pl={2}
|
||||||
|
pb={2}
|
||||||
|
fontSize="xs"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="gray.500"
|
||||||
|
display={{ base: "none", md: "flex" }}
|
||||||
|
>
|
||||||
|
CONFIGURATION
|
||||||
|
</Text>
|
||||||
|
<IconLink icon={BsGearFill} label="Project Settings" href="/project/settings" />
|
||||||
|
</VStack>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{user === null && (
|
{user === null && (
|
||||||
<NavSidebarOption>
|
<NavSidebarOption>
|
||||||
<HStack
|
<HStack
|
||||||
w="full"
|
w="full"
|
||||||
p={4}
|
p={{ base: 2, md: 4 }}
|
||||||
as={ChakraLink}
|
as={ChakraLink}
|
||||||
justifyContent="start"
|
justifyContent="start"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -87,20 +111,7 @@ const NavSidebar = () => {
|
|||||||
</NavSidebarOption>
|
</NavSidebarOption>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
<VStack w="full" alignItems="flex-start" spacing={0}>
|
|
||||||
<Text
|
|
||||||
pl={2}
|
|
||||||
pb={2}
|
|
||||||
fontSize="xs"
|
|
||||||
fontWeight="bold"
|
|
||||||
color="gray.500"
|
|
||||||
display={{ base: "none", md: "flex" }}
|
|
||||||
>
|
|
||||||
CONFIGURATION
|
|
||||||
</Text>
|
|
||||||
<IconLink icon={BsGearFill} label="Project Settings" href="/project/settings" />
|
|
||||||
</VStack>
|
|
||||||
{user && <UserMenu user={user} borderColor={"gray.200"} />}
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<VStack spacing={0} align="center">
|
<VStack spacing={0} align="center">
|
||||||
<ChakraLink
|
<ChakraLink
|
||||||
@@ -121,10 +132,12 @@ export default function AppShell({
|
|||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
|
requireBeta,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
title?: string;
|
title?: string;
|
||||||
requireAuth?: boolean;
|
requireAuth?: boolean;
|
||||||
|
requireBeta?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const [vh, setVh] = useState("100vh"); // Default height to prevent flicker on initial render
|
const [vh, setVh] = useState("100vh"); // Default height to prevent flicker on initial render
|
||||||
|
|
||||||
@@ -154,15 +167,21 @@ export default function AppShell({
|
|||||||
}
|
}
|
||||||
}, [requireAuth, user, authLoading]);
|
}, [requireAuth, user, authLoading]);
|
||||||
|
|
||||||
|
const flags = useAppStore((s) => s.featureFlags.featureFlags);
|
||||||
|
const flagsLoaded = useAppStore((s) => s.featureFlags.flagsLoaded);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h={vh} w="100vw">
|
<>
|
||||||
<Head>
|
<Flex h={vh} w="100vw">
|
||||||
<title>{title ? `${title} | OpenPipe` : "OpenPipe"}</title>
|
<Head>
|
||||||
</Head>
|
<title>{title ? `${title} | OpenPipe` : "OpenPipe"}</title>
|
||||||
<NavSidebar />
|
</Head>
|
||||||
<Box h="100%" flex={1} overflowY="auto">
|
<NavSidebar />
|
||||||
{children}
|
<Box h="100%" flex={1} overflowY="auto" bgColor="gray.50">
|
||||||
</Box>
|
{children}
|
||||||
</Flex>
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
{requireBeta && flagsLoaded && !flags.betaAccess && <BetaModal />}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
67
app/src/components/nav/BetaModal.tsx
Normal file
67
app/src/components/nav/BetaModal.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
VStack,
|
||||||
|
Text,
|
||||||
|
HStack,
|
||||||
|
Icon,
|
||||||
|
Link,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { BsStars } from "react-icons/bs";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
|
export const BetaModal = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
const email = session.data?.user.email ?? "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen
|
||||||
|
onClose={router.back}
|
||||||
|
closeOnOverlayClick={false}
|
||||||
|
size={{ base: "xl", md: "2xl" }}
|
||||||
|
>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent w={1200}>
|
||||||
|
<ModalHeader>
|
||||||
|
<HStack>
|
||||||
|
<Icon as={BsStars} />
|
||||||
|
<Text>Beta-Only Feature</Text>
|
||||||
|
</HStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody maxW="unset">
|
||||||
|
<VStack spacing={8} py={4} alignItems="flex-start">
|
||||||
|
<Text fontSize="md">
|
||||||
|
This feature is currently in beta. To receive early access to beta-only features, join
|
||||||
|
the waitlist. You'll receive an email at <b>{email}</b> when you're approved.
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<HStack spacing={4}>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
textDecoration="none !important"
|
||||||
|
colorScheme="orange"
|
||||||
|
target="_blank"
|
||||||
|
href={`https://ax3nafkw0jp.typeform.com/to/ZNpYqvAc#email=${email}`}
|
||||||
|
>
|
||||||
|
Join Waitlist
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="blue" onClick={router.back}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,16 +6,19 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
Flex,
|
Flex,
|
||||||
IconButton,
|
|
||||||
Icon,
|
Icon,
|
||||||
Divider,
|
Divider,
|
||||||
Button,
|
Button,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
Link as ChakraLink,
|
||||||
|
Image,
|
||||||
|
Box,
|
||||||
|
Portal,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BsChevronRight, BsGear, BsPlus } from "react-icons/bs";
|
import { BsPlus, BsPersonCircle } from "react-icons/bs";
|
||||||
import { type Project } from "@prisma/client";
|
import { type Project } from "@prisma/client";
|
||||||
|
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
@@ -23,13 +26,14 @@ import { api } from "~/utils/api";
|
|||||||
import NavSidebarOption from "./NavSidebarOption";
|
import NavSidebarOption from "./NavSidebarOption";
|
||||||
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useSession, signOut } from "next-auth/react";
|
||||||
|
|
||||||
export default function ProjectMenu() {
|
export default function ProjectMenu() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
|
|
||||||
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
||||||
const setselectedProjectId = useAppStore((s) => s.setselectedProjectId);
|
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
|
||||||
|
|
||||||
const { data: projects } = api.projects.list.useQuery();
|
const { data: projects } = api.projects.list.useQuery();
|
||||||
|
|
||||||
@@ -39,9 +43,9 @@ export default function ProjectMenu() {
|
|||||||
projects[0] &&
|
projects[0] &&
|
||||||
(!selectedProjectId || !projects.find((proj) => proj.id === selectedProjectId))
|
(!selectedProjectId || !projects.find((proj) => proj.id === selectedProjectId))
|
||||||
) {
|
) {
|
||||||
setselectedProjectId(projects[0].id);
|
setSelectedProjectId(projects[0].id);
|
||||||
}
|
}
|
||||||
}, [selectedProjectId, setselectedProjectId, projects]);
|
}, [selectedProjectId, setSelectedProjectId, projects]);
|
||||||
|
|
||||||
const { data: selectedProject } = useSelectedProject();
|
const { data: selectedProject } = useSelectedProject();
|
||||||
|
|
||||||
@@ -49,28 +53,38 @@ export default function ProjectMenu() {
|
|||||||
|
|
||||||
const createMutation = api.projects.create.useMutation();
|
const createMutation = api.projects.create.useMutation();
|
||||||
const [createProject, isLoading] = useHandledAsyncCallback(async () => {
|
const [createProject, isLoading] = useHandledAsyncCallback(async () => {
|
||||||
const newProj = await createMutation.mutateAsync({ name: "New Project" });
|
const newProj = await createMutation.mutateAsync({ name: "Untitled Project" });
|
||||||
await utils.projects.list.invalidate();
|
await utils.projects.list.invalidate();
|
||||||
setselectedProjectId(newProj.id);
|
setSelectedProjectId(newProj.id);
|
||||||
await router.push({ pathname: "/project/settings" });
|
await router.push({ pathname: "/project/settings" });
|
||||||
}, [createMutation, router]);
|
}, [createMutation, router]);
|
||||||
|
|
||||||
|
const user = useSession().data;
|
||||||
|
|
||||||
|
const profileImage = user?.user.image ? (
|
||||||
|
<Image src={user.user.image} alt="profile picture" boxSize={6} borderRadius="50%" />
|
||||||
|
) : (
|
||||||
|
<Icon as={BsPersonCircle} boxSize={6} />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack w="full" alignItems="flex-start" spacing={0}>
|
<VStack
|
||||||
<Text
|
w="full"
|
||||||
pl={2}
|
alignItems="flex-start"
|
||||||
pb={2}
|
spacing={0}
|
||||||
fontSize="xs"
|
py={1}
|
||||||
fontWeight="bold"
|
zIndex={popover.isOpen ? "dropdown" : undefined}
|
||||||
color="gray.500"
|
>
|
||||||
display={{ base: "none", md: "flex" }}
|
<Popover
|
||||||
|
placement="bottom"
|
||||||
|
isOpen={popover.isOpen}
|
||||||
|
onOpen={popover.onOpen}
|
||||||
|
onClose={popover.onClose}
|
||||||
|
closeOnBlur
|
||||||
>
|
>
|
||||||
PROJECT
|
|
||||||
</Text>
|
|
||||||
<Popover placement="right-end" isOpen={popover.isOpen} onClose={popover.onClose} closeOnBlur>
|
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<NavSidebarOption>
|
<NavSidebarOption>
|
||||||
<HStack w="full" onClick={popover.onToggle}>
|
<HStack w="full">
|
||||||
<Flex
|
<Flex
|
||||||
p={1}
|
p={1}
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
@@ -83,43 +97,79 @@ export default function ProjectMenu() {
|
|||||||
>
|
>
|
||||||
<Text>{selectedProject?.name[0]?.toUpperCase()}</Text>
|
<Text>{selectedProject?.name[0]?.toUpperCase()}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Text fontSize="sm" display={{ base: "none", md: "block" }} py={1} flex={1}>
|
<Text
|
||||||
|
fontSize="sm"
|
||||||
|
display={{ base: "none", md: "block" }}
|
||||||
|
py={1}
|
||||||
|
flex={1}
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
{selectedProject?.name}
|
{selectedProject?.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Icon as={BsChevronRight} boxSize={4} color="gray.500" />
|
<Box mr={2}>{profileImage}</Box>
|
||||||
</HStack>
|
</HStack>
|
||||||
</NavSidebarOption>
|
</NavSidebarOption>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent _focusVisible={{ outline: "unset" }} ml={-1} w="auto" minW={100} maxW={280}>
|
<Portal>
|
||||||
<VStack alignItems="flex-start" spacing={2} py={4} px={2}>
|
<PopoverContent
|
||||||
<Text color="gray.500" fontSize="xs" fontWeight="bold" pb={1}>
|
_focusVisible={{ outline: "unset" }}
|
||||||
PROJECTS
|
w={220}
|
||||||
</Text>
|
ml={{ base: 2, md: 0 }}
|
||||||
<Divider />
|
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
|
||||||
<VStack spacing={0} w="full">
|
fontSize="sm"
|
||||||
{projects?.map((proj) => (
|
>
|
||||||
<ProjectOption
|
<VStack alignItems="flex-start" spacing={1} py={1}>
|
||||||
key={proj.id}
|
<Text px={3} py={2}>
|
||||||
proj={proj}
|
{user?.user.email}
|
||||||
isActive={proj.id === selectedProjectId}
|
</Text>
|
||||||
onClose={popover.onClose}
|
<Divider />
|
||||||
/>
|
<Text alignSelf="flex-start" fontWeight="bold" px={3} pt={2}>
|
||||||
))}
|
Your Projects
|
||||||
|
</Text>
|
||||||
|
<VStack spacing={0} w="full" px={1}>
|
||||||
|
{projects?.map((proj) => (
|
||||||
|
<ProjectOption
|
||||||
|
key={proj.id}
|
||||||
|
proj={proj}
|
||||||
|
isActive={proj.id === selectedProjectId}
|
||||||
|
onClose={popover.onClose}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<HStack
|
||||||
|
as={Button}
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="blue"
|
||||||
|
color="blue.400"
|
||||||
|
fontSize="sm"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
onClick={createProject}
|
||||||
|
w="full"
|
||||||
|
borderRadius={4}
|
||||||
|
spacing={0}
|
||||||
|
>
|
||||||
|
<Text>Add project</Text>
|
||||||
|
<Icon as={isLoading ? Spinner : BsPlus} boxSize={4} strokeWidth={0.5} />
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<VStack w="full" px={1}>
|
||||||
|
<ChakraLink
|
||||||
|
onClick={() => {
|
||||||
|
signOut().catch(console.error);
|
||||||
|
}}
|
||||||
|
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
|
||||||
|
w="full"
|
||||||
|
py={2}
|
||||||
|
px={2}
|
||||||
|
borderRadius={4}
|
||||||
|
>
|
||||||
|
<Text>Sign out</Text>
|
||||||
|
</ChakraLink>
|
||||||
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
<HStack
|
</PopoverContent>
|
||||||
as={Button}
|
</Portal>
|
||||||
variant="ghost"
|
|
||||||
colorScheme="blue"
|
|
||||||
color="blue.400"
|
|
||||||
pr={8}
|
|
||||||
w="full"
|
|
||||||
onClick={createProject}
|
|
||||||
>
|
|
||||||
<Icon as={isLoading ? Spinner : BsPlus} boxSize={6} />
|
|
||||||
<Text>New project</Text>
|
|
||||||
</HStack>
|
|
||||||
</VStack>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
@@ -134,38 +184,26 @@ const ProjectOption = ({
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const setselectedProjectId = useAppStore((s) => s.setselectedProjectId);
|
const setSelectedProjectId = useAppStore((s) => s.setSelectedProjectId);
|
||||||
const [gearHovered, setGearHovered] = useState(false);
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
as={Link}
|
as={Link}
|
||||||
href="/experiments"
|
href="/experiments"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setselectedProjectId(proj.id);
|
setSelectedProjectId(proj.id);
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
w="full"
|
w="full"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
bgColor={isActive ? "gray.100" : "transparent"}
|
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
|
||||||
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
|
bgColor={isActive ? "gray.100" : undefined}
|
||||||
p={2}
|
py={2}
|
||||||
|
px={4}
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
spacing={4}
|
spacing={4}
|
||||||
>
|
>
|
||||||
<Text>{proj.name}</Text>
|
<Text>{proj.name}</Text>
|
||||||
<IconButton
|
|
||||||
as={Link}
|
|
||||||
href="/project/settings"
|
|
||||||
aria-label={`Open ${proj.name} settings`}
|
|
||||||
icon={<Icon as={BsGear} boxSize={5} strokeWidth={0.5} color="gray.500" />}
|
|
||||||
variant="ghost"
|
|
||||||
size="xs"
|
|
||||||
p={0}
|
|
||||||
onMouseEnter={() => setGearHovered(true)}
|
|
||||||
onMouseLeave={() => setGearHovered(false)}
|
|
||||||
_hover={{ bgColor: isActive ? "gray.300" : "gray.100", transitionDelay: 0 }}
|
|
||||||
borderRadius={4}
|
|
||||||
/>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,50 +23,48 @@ export default function UserMenu({ user, ...rest }: { user: Session } & StackPro
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Popover placement="right">
|
||||||
<Popover placement="right">
|
<PopoverTrigger>
|
||||||
<PopoverTrigger>
|
<NavSidebarOption>
|
||||||
<NavSidebarOption>
|
<HStack
|
||||||
<HStack
|
// Weird values to make mobile look right; can clean up when we make the sidebar disappear on mobile
|
||||||
// Weird values to make mobile look right; can clean up when we make the sidebar disappear on mobile
|
py={2}
|
||||||
py={2}
|
px={1}
|
||||||
px={1}
|
spacing={3}
|
||||||
spacing={3}
|
{...rest}
|
||||||
{...rest}
|
>
|
||||||
>
|
{profileImage}
|
||||||
{profileImage}
|
<VStack spacing={0} align="start" flex={1} flexShrink={1}>
|
||||||
<VStack spacing={0} align="start" flex={1} flexShrink={1}>
|
<Text fontWeight="bold" fontSize="sm">
|
||||||
<Text fontWeight="bold" fontSize="sm">
|
{user.user.name}
|
||||||
{user.user.name}
|
</Text>
|
||||||
</Text>
|
<Text color="gray.500" fontSize="xs">
|
||||||
<Text color="gray.500" fontSize="xs">
|
{/* {user.user.email} */}
|
||||||
{/* {user.user.email} */}
|
</Text>
|
||||||
</Text>
|
</VStack>
|
||||||
</VStack>
|
<Icon as={BsChevronRight} boxSize={4} color="gray.500" />
|
||||||
<Icon as={BsChevronRight} boxSize={4} color="gray.500" />
|
</HStack>
|
||||||
</HStack>
|
</NavSidebarOption>
|
||||||
</NavSidebarOption>
|
</PopoverTrigger>
|
||||||
</PopoverTrigger>
|
<PopoverContent _focusVisible={{ outline: "unset" }} ml={-1} minW={48} w="full">
|
||||||
<PopoverContent _focusVisible={{ outline: "unset" }} ml={-1} minW={48} w="full">
|
<VStack align="stretch" spacing={0}>
|
||||||
<VStack align="stretch" spacing={0}>
|
{/* sign out */}
|
||||||
{/* sign out */}
|
<HStack
|
||||||
<HStack
|
as={Link}
|
||||||
as={Link}
|
onClick={() => {
|
||||||
onClick={() => {
|
signOut().catch(console.error);
|
||||||
signOut().catch(console.error);
|
}}
|
||||||
}}
|
px={4}
|
||||||
px={4}
|
py={2}
|
||||||
py={2}
|
spacing={4}
|
||||||
spacing={4}
|
color="gray.500"
|
||||||
color="gray.500"
|
fontSize="sm"
|
||||||
fontSize="sm"
|
>
|
||||||
>
|
<Icon as={BsBoxArrowRight} boxSize={6} />
|
||||||
<Icon as={BsBoxArrowRight} boxSize={6} />
|
<Text>Sign out</Text>
|
||||||
<Text>Sign out</Text>
|
</HStack>
|
||||||
</HStack>
|
</VStack>
|
||||||
</VStack>
|
</PopoverContent>
|
||||||
</PopoverContent>
|
</Popover>
|
||||||
</Popover>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
128
app/src/components/projectSettings/InviteMemberModal.tsx
Normal file
128
app/src/components/projectSettings/InviteMemberModal.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
FormHelperText,
|
||||||
|
HStack,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Spinner,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
RadioGroup,
|
||||||
|
Radio,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
||||||
|
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
||||||
|
import { type ProjectUserRole } from "@prisma/client";
|
||||||
|
|
||||||
|
export const InviteMemberModal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
const selectedProject = useSelectedProject().data;
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [role, setRole] = useState<ProjectUserRole>("MEMBER");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEmail("");
|
||||||
|
setRole("MEMBER");
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const emailIsValid = !email || !email.match(/.+@.+\..+/);
|
||||||
|
|
||||||
|
const inviteMemberMutation = api.users.inviteToProject.useMutation();
|
||||||
|
|
||||||
|
const [inviteMember, isInviting] = useHandledAsyncCallback(async () => {
|
||||||
|
if (!selectedProject?.id || !role) return;
|
||||||
|
const resp = await inviteMemberMutation.mutateAsync({
|
||||||
|
projectId: selectedProject.id,
|
||||||
|
email,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
if (maybeReportError(resp)) return;
|
||||||
|
await utils.projects.get.invalidate();
|
||||||
|
onClose();
|
||||||
|
}, [inviteMemberMutation, email, role, selectedProject?.id, onClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent w={1200}>
|
||||||
|
<ModalHeader>
|
||||||
|
<HStack>
|
||||||
|
<Text>Invite Member</Text>
|
||||||
|
</HStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<VStack spacing={8} alignItems="flex-start">
|
||||||
|
<Text>
|
||||||
|
Invite a new member to <b>{selectedProject?.name}</b>.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
value={role}
|
||||||
|
onChange={(e) => setRole(e as ProjectUserRole)}
|
||||||
|
colorScheme="orange"
|
||||||
|
>
|
||||||
|
<VStack w="full" alignItems="flex-start">
|
||||||
|
<Radio value="MEMBER">
|
||||||
|
<Text fontSize="sm">MEMBER</Text>
|
||||||
|
</Radio>
|
||||||
|
<Radio value="ADMIN">
|
||||||
|
<Text fontSize="sm">ADMIN</Text>
|
||||||
|
</Radio>
|
||||||
|
</VStack>
|
||||||
|
</RadioGroup>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey || e.shiftKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.blur();
|
||||||
|
inviteMember();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormHelperText>Enter the email of the person you want to invite.</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</VStack>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter mt={4}>
|
||||||
|
<HStack>
|
||||||
|
<Button colorScheme="gray" onClick={onClose} minW={24}>
|
||||||
|
<Text>Cancel</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorScheme="orange"
|
||||||
|
onClick={inviteMember}
|
||||||
|
minW={24}
|
||||||
|
isDisabled={emailIsValid || isInviting}
|
||||||
|
>
|
||||||
|
{isInviting ? <Spinner boxSize={4} /> : <Text>Send Invitation</Text>}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
145
app/src/components/projectSettings/MemberTable.tsx
Normal file
145
app/src/components/projectSettings/MemberTable.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
IconButton,
|
||||||
|
useDisclosure,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { BsTrash } from "react-icons/bs";
|
||||||
|
import { type User } from "@prisma/client";
|
||||||
|
|
||||||
|
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
||||||
|
import { InviteMemberModal } from "./InviteMemberModal";
|
||||||
|
import { RemoveMemberDialog } from "./RemoveMemberDialog";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
||||||
|
|
||||||
|
const MemberTable = () => {
|
||||||
|
const selectedProject = useSelectedProject().data;
|
||||||
|
const session = useSession().data;
|
||||||
|
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
|
const [memberToRemove, setMemberToRemove] = useState<User | null>(null);
|
||||||
|
const inviteMemberModal = useDisclosure();
|
||||||
|
|
||||||
|
const cancelInvitationMutation = api.users.cancelProjectInvitation.useMutation();
|
||||||
|
|
||||||
|
const [cancelInvitation, isCancelling] = useHandledAsyncCallback(
|
||||||
|
async (invitationToken: string) => {
|
||||||
|
if (!selectedProject?.id) return;
|
||||||
|
const resp = await cancelInvitationMutation.mutateAsync({
|
||||||
|
invitationToken,
|
||||||
|
});
|
||||||
|
if (maybeReportError(resp)) return;
|
||||||
|
await utils.projects.get.invalidate();
|
||||||
|
},
|
||||||
|
[selectedProject?.id, cancelInvitationMutation],
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedMembers = useMemo(() => {
|
||||||
|
if (!selectedProject?.projectUsers) return [];
|
||||||
|
return selectedProject.projectUsers.sort((a, b) => {
|
||||||
|
if (a.role === b.role) return a.createdAt < b.createdAt ? -1 : 1;
|
||||||
|
// Take advantage of fact that ADMIN is alphabetically before MEMBER
|
||||||
|
return a.role < b.role ? -1 : 1;
|
||||||
|
});
|
||||||
|
}, [selectedProject?.projectUsers]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table fontSize={{ base: "sm", md: "md" }}>
|
||||||
|
<Thead
|
||||||
|
sx={{
|
||||||
|
th: {
|
||||||
|
base: { px: 0 },
|
||||||
|
md: { px: 6 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th display={{ base: "none", md: "table-cell" }}>Email</Th>
|
||||||
|
<Th>Role</Th>
|
||||||
|
{selectedProject?.role === "ADMIN" && <Th />}
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody
|
||||||
|
sx={{
|
||||||
|
td: {
|
||||||
|
base: { px: 0 },
|
||||||
|
md: { px: 6 },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedProject &&
|
||||||
|
sortedMembers.map((member) => {
|
||||||
|
return (
|
||||||
|
<Tr key={member.id}>
|
||||||
|
<Td>
|
||||||
|
<Text fontWeight="bold">{member.user.name}</Text>
|
||||||
|
</Td>
|
||||||
|
<Td display={{ base: "none", md: "table-cell" }} h="full">
|
||||||
|
{member.user.email}
|
||||||
|
</Td>
|
||||||
|
<Td fontSize={{ base: "xs", md: "sm" }}>{member.role}</Td>
|
||||||
|
{selectedProject.role === "ADMIN" && (
|
||||||
|
<Td textAlign="end">
|
||||||
|
{member.user.id !== session?.user?.id &&
|
||||||
|
member.user.id !== selectedProject.personalProjectUserId && (
|
||||||
|
<IconButton
|
||||||
|
aria-label="Remove member"
|
||||||
|
colorScheme="red"
|
||||||
|
icon={<BsTrash />}
|
||||||
|
onClick={() => setMemberToRemove(member.user)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{selectedProject?.projectUserInvitations?.map((invitation) => {
|
||||||
|
return (
|
||||||
|
<Tr key={invitation.id}>
|
||||||
|
<Td>
|
||||||
|
<Text as="i">Invitation pending</Text>
|
||||||
|
</Td>
|
||||||
|
<Td>{invitation.email}</Td>
|
||||||
|
<Td fontSize="sm">{invitation.role}</Td>
|
||||||
|
{selectedProject.role === "ADMIN" && (
|
||||||
|
<Td textAlign="end">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
colorScheme="red"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => cancelInvitation(invitation.invitationToken)}
|
||||||
|
isLoading={isCancelling}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
<InviteMemberModal isOpen={inviteMemberModal.isOpen} onClose={inviteMemberModal.onClose} />
|
||||||
|
<RemoveMemberDialog
|
||||||
|
member={memberToRemove}
|
||||||
|
isOpen={!!memberToRemove}
|
||||||
|
onClose={() => setMemberToRemove(null)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemberTable;
|
||||||
71
app/src/components/projectSettings/RemoveMemberDialog.tsx
Normal file
71
app/src/components/projectSettings/RemoveMemberDialog.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
Text,
|
||||||
|
VStack,
|
||||||
|
Spinner,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { type User } from "@prisma/client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useHandledAsyncCallback, useSelectedProject } from "~/utils/hooks";
|
||||||
|
|
||||||
|
export const RemoveMemberDialog = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
member,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
member: User | null;
|
||||||
|
}) => {
|
||||||
|
const selectedProject = useSelectedProject();
|
||||||
|
const removeUserMutation = api.users.removeUserFromProject.useMutation();
|
||||||
|
const utils = api.useContext();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const [onRemoveConfirm, isRemoving] = useHandledAsyncCallback(async () => {
|
||||||
|
if (!selectedProject.data?.id || !member?.id) return;
|
||||||
|
await removeUserMutation.mutateAsync({ projectId: selectedProject.data.id, userId: member.id });
|
||||||
|
await utils.projects.get.invalidate();
|
||||||
|
onClose();
|
||||||
|
}, [removeUserMutation, selectedProject, router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
Remove Member
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<AlertDialogBody>
|
||||||
|
<VStack spacing={4} alignItems="flex-start">
|
||||||
|
<Text>
|
||||||
|
Are you sure you want to remove <b>{member?.name}</b> from the project?
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</AlertDialogBody>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button ref={cancelRef} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="red" onClick={onRemoveConfirm} ml={3} w={20}>
|
||||||
|
{isRemoving ? <Spinner /> : "Remove"}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
30
app/src/components/requestLogs/ActionButton.tsx
Normal file
30
app/src/components/requestLogs/ActionButton.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Button, HStack, type ButtonProps, Icon, Text } from "@chakra-ui/react";
|
||||||
|
import { type IconType } from "react-icons";
|
||||||
|
|
||||||
|
const ActionButton = ({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
...buttonProps
|
||||||
|
}: { icon: IconType; label: string } & ButtonProps) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
color="black"
|
||||||
|
bgColor="white"
|
||||||
|
borderColor="gray.300"
|
||||||
|
borderRadius={4}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
fontSize="sm"
|
||||||
|
fontWeight="normal"
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
{icon && <Icon as={icon} />}
|
||||||
|
<Text display={{ base: "none", md: "flex" }}>{label}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionButton;
|
||||||
117
app/src/components/requestLogs/ColumnVisiblityDropdown.tsx
Normal file
117
app/src/components/requestLogs/ColumnVisiblityDropdown.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
Box,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { BiCheck } from "react-icons/bi";
|
||||||
|
import { BsToggles } from "react-icons/bs";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { useIsClientRehydrated, useTagNames } from "~/utils/hooks";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import { StaticColumnKeys } from "~/state/columnVisiblitySlice";
|
||||||
|
import ActionButton from "./ActionButton";
|
||||||
|
|
||||||
|
const ColumnVisiblityDropdown = () => {
|
||||||
|
const tagNames = useTagNames().data;
|
||||||
|
|
||||||
|
const visibleColumns = useAppStore((s) => s.columnVisibility.visibleColumns);
|
||||||
|
const toggleColumnVisibility = useAppStore((s) => s.columnVisibility.toggleColumnVisibility);
|
||||||
|
const totalColumns = Object.keys(StaticColumnKeys).length + (tagNames?.length ?? 0);
|
||||||
|
|
||||||
|
const popover = useDisclosure();
|
||||||
|
|
||||||
|
const columnVisiblityOptions = useMemo(() => {
|
||||||
|
const options: { label: string; key: string }[] = [
|
||||||
|
{
|
||||||
|
label: "Sent At",
|
||||||
|
key: StaticColumnKeys.SENT_AT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Model",
|
||||||
|
key: StaticColumnKeys.MODEL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Duration",
|
||||||
|
key: StaticColumnKeys.DURATION,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Input Tokens",
|
||||||
|
key: StaticColumnKeys.INPUT_TOKENS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Output Tokens",
|
||||||
|
key: StaticColumnKeys.OUTPUT_TOKENS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Status Code",
|
||||||
|
key: StaticColumnKeys.STATUS_CODE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (const tagName of tagNames ?? []) {
|
||||||
|
options.push({
|
||||||
|
label: tagName,
|
||||||
|
key: tagName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}, [tagNames]);
|
||||||
|
|
||||||
|
const isClientRehydrated = useIsClientRehydrated();
|
||||||
|
if (!isClientRehydrated) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
placement="bottom-start"
|
||||||
|
isOpen={popover.isOpen}
|
||||||
|
onOpen={popover.onOpen}
|
||||||
|
onClose={popover.onClose}
|
||||||
|
>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Box>
|
||||||
|
<ActionButton
|
||||||
|
label={`Columns (${visibleColumns.size}/${totalColumns})`}
|
||||||
|
icon={BsToggles}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);" minW={0} w="auto">
|
||||||
|
<VStack spacing={0} maxH={400} overflowY="auto">
|
||||||
|
{columnVisiblityOptions?.map((option, index) => (
|
||||||
|
<HStack
|
||||||
|
key={index}
|
||||||
|
as={Button}
|
||||||
|
onClick={() => toggleColumnVisibility(option.key)}
|
||||||
|
w="full"
|
||||||
|
minH={10}
|
||||||
|
variant="ghost"
|
||||||
|
justifyContent="space-between"
|
||||||
|
fontWeight="semibold"
|
||||||
|
borderRadius={0}
|
||||||
|
colorScheme="blue"
|
||||||
|
color="black"
|
||||||
|
fontSize="sm"
|
||||||
|
borderBottomWidth={1}
|
||||||
|
>
|
||||||
|
<Text mr={16}>{option.label}</Text>
|
||||||
|
<Box w={5}>
|
||||||
|
{visibleColumns.has(option.key) && (
|
||||||
|
<Icon as={BiCheck} color="blue.500" boxSize={5} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColumnVisiblityDropdown;
|
||||||
210
app/src/components/requestLogs/ExportButton.tsx
Normal file
210
app/src/components/requestLogs/ExportButton.tsx
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Icon,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberDecrementStepper,
|
||||||
|
Collapse,
|
||||||
|
Flex,
|
||||||
|
useDisclosure,
|
||||||
|
type UseDisclosureReturn,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { BiExport } from "react-icons/bi";
|
||||||
|
|
||||||
|
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import ActionButton from "./ActionButton";
|
||||||
|
import InputDropdown from "../InputDropdown";
|
||||||
|
import { FiChevronUp, FiChevronDown } from "react-icons/fi";
|
||||||
|
import InfoCircle from "../InfoCircle";
|
||||||
|
|
||||||
|
const SUPPORTED_EXPORT_FORMATS = ["alpaca-finetune", "openai-fine-tune", "unformatted"];
|
||||||
|
|
||||||
|
const ExportButton = () => {
|
||||||
|
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
||||||
|
|
||||||
|
const disclosure = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionButton
|
||||||
|
onClick={disclosure.onOpen}
|
||||||
|
label="Export"
|
||||||
|
icon={BiExport}
|
||||||
|
isDisabled={selectedLogIds.size === 0}
|
||||||
|
/>
|
||||||
|
<ExportLogsModal disclosure={disclosure} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExportButton;
|
||||||
|
|
||||||
|
const ExportLogsModal = ({ disclosure }: { disclosure: UseDisclosureReturn }) => {
|
||||||
|
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
||||||
|
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
||||||
|
const clearSelectedLogIds = useAppStore((s) => s.selectedLogs.clearSelectedLogIds);
|
||||||
|
|
||||||
|
const [selectedExportFormat, setSelectedExportFormat] = useState(SUPPORTED_EXPORT_FORMATS[0]);
|
||||||
|
const [testingSplit, setTestingSplit] = useState(10);
|
||||||
|
const [removeDuplicates, setRemoveDuplicates] = useState(true);
|
||||||
|
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disclosure.isOpen) {
|
||||||
|
setSelectedExportFormat(SUPPORTED_EXPORT_FORMATS[0]);
|
||||||
|
setTestingSplit(10);
|
||||||
|
setRemoveDuplicates(true);
|
||||||
|
}
|
||||||
|
}, [disclosure.isOpen]);
|
||||||
|
|
||||||
|
const exportLogsMutation = api.loggedCalls.export.useMutation();
|
||||||
|
|
||||||
|
const [exportLogs, exportInProgress] = useHandledAsyncCallback(async () => {
|
||||||
|
if (!selectedProjectId || !selectedLogIds.size || !testingSplit || !selectedExportFormat)
|
||||||
|
return;
|
||||||
|
const response = await exportLogsMutation.mutateAsync({
|
||||||
|
projectId: selectedProjectId,
|
||||||
|
selectedLogIds: Array.from(selectedLogIds),
|
||||||
|
testingSplit,
|
||||||
|
selectedExportFormat,
|
||||||
|
removeDuplicates,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataUrl = `data:application/pdf;base64,${response}`;
|
||||||
|
const blob = await fetch(dataUrl).then((res) => res.blob());
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
|
||||||
|
a.href = url;
|
||||||
|
a.download = `data.zip`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
disclosure.onClose();
|
||||||
|
clearSelectedLogIds();
|
||||||
|
}, [
|
||||||
|
exportLogsMutation,
|
||||||
|
selectedProjectId,
|
||||||
|
selectedLogIds,
|
||||||
|
testingSplit,
|
||||||
|
selectedExportFormat,
|
||||||
|
removeDuplicates,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal size={{ base: "xl", md: "2xl" }} {...disclosure}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent w={1200}>
|
||||||
|
<ModalHeader>
|
||||||
|
<HStack>
|
||||||
|
<Icon as={BiExport} />
|
||||||
|
<Text>Export Logs</Text>
|
||||||
|
</HStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody maxW="unset">
|
||||||
|
<VStack w="full" spacing={8} pt={4} alignItems="flex-start">
|
||||||
|
<Text>
|
||||||
|
We'll export the <b>{selectedLogIds.size}</b> logs you have selected in the format of
|
||||||
|
your choice.
|
||||||
|
</Text>
|
||||||
|
<VStack alignItems="flex-start" spacing={4}>
|
||||||
|
<Flex
|
||||||
|
flexDir={{ base: "column", md: "row" }}
|
||||||
|
alignItems={{ base: "flex-start", md: "center" }}
|
||||||
|
>
|
||||||
|
<HStack w={48} alignItems="center" spacing={1}>
|
||||||
|
<Text fontWeight="bold">Format:</Text>
|
||||||
|
<InfoCircle tooltipText="Format logs for for fine tuning or export them without formatting." />
|
||||||
|
</HStack>
|
||||||
|
<InputDropdown
|
||||||
|
options={SUPPORTED_EXPORT_FORMATS}
|
||||||
|
selectedOption={selectedExportFormat}
|
||||||
|
onSelect={(option) => setSelectedExportFormat(option)}
|
||||||
|
inputGroupProps={{ w: 48 }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
flexDir={{ base: "column", md: "row" }}
|
||||||
|
alignItems={{ base: "flex-start", md: "center" }}
|
||||||
|
>
|
||||||
|
<HStack w={48} alignItems="center" spacing={1}>
|
||||||
|
<Text fontWeight="bold">Testing Split:</Text>
|
||||||
|
<InfoCircle tooltipText="The percent of your logs that will be reserved for testing and saved in another file. Logs are split randomly." />
|
||||||
|
</HStack>
|
||||||
|
<HStack>
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={10}
|
||||||
|
onChange={(_, num) => setTestingSplit(num)}
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
w={48}
|
||||||
|
>
|
||||||
|
<NumberInputField />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</VStack>
|
||||||
|
<VStack alignItems="flex-start" spacing={0}>
|
||||||
|
<Button
|
||||||
|
variant="unstyled"
|
||||||
|
color="blue.600"
|
||||||
|
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
||||||
|
>
|
||||||
|
<HStack>
|
||||||
|
<Text>Advanced Options</Text>
|
||||||
|
<Icon as={showAdvancedOptions ? FiChevronUp : FiChevronDown} />
|
||||||
|
</HStack>
|
||||||
|
</Button>
|
||||||
|
<Collapse in={showAdvancedOptions} unmountOnExit={true}>
|
||||||
|
<VStack align="stretch" pt={4}>
|
||||||
|
<HStack>
|
||||||
|
<Checkbox
|
||||||
|
colorScheme="blue"
|
||||||
|
isChecked={removeDuplicates}
|
||||||
|
onChange={(e) => setRemoveDuplicates(e.target.checked)}
|
||||||
|
>
|
||||||
|
<Text>Remove duplicates</Text>
|
||||||
|
</Checkbox>
|
||||||
|
<InfoCircle tooltipText="To avoid overfitting and speed up training, automatically deduplicate logs with matching input and output." />
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
</Collapse>
|
||||||
|
</VStack>
|
||||||
|
</VStack>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<HStack>
|
||||||
|
<Button colorScheme="gray" onClick={disclosure.onClose} minW={24}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="blue" onClick={exportLogs} isLoading={exportInProgress} minW={24}>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
161
app/src/components/requestLogs/FineTuneButton.tsx
Normal file
161
app/src/components/requestLogs/FineTuneButton.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Icon,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
useDisclosure,
|
||||||
|
type UseDisclosureReturn,
|
||||||
|
Input,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { FaRobot } from "react-icons/fa";
|
||||||
|
import humanId from "human-id";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import ActionButton from "./ActionButton";
|
||||||
|
import InputDropdown from "../InputDropdown";
|
||||||
|
import { FiChevronDown } from "react-icons/fi";
|
||||||
|
|
||||||
|
const SUPPORTED_BASE_MODELS = ["llama2-7b", "llama2-13b", "llama2-70b", "gpt-3.5-turbo"];
|
||||||
|
|
||||||
|
const FineTuneButton = () => {
|
||||||
|
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
||||||
|
|
||||||
|
const disclosure = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionButton
|
||||||
|
onClick={disclosure.onOpen}
|
||||||
|
label="Fine Tune"
|
||||||
|
icon={FaRobot}
|
||||||
|
isDisabled={selectedLogIds.size === 0}
|
||||||
|
/>
|
||||||
|
<FineTuneModal disclosure={disclosure} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FineTuneButton;
|
||||||
|
|
||||||
|
const FineTuneModal = ({ disclosure }: { disclosure: UseDisclosureReturn }) => {
|
||||||
|
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
||||||
|
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
||||||
|
const clearSelectedLogIds = useAppStore((s) => s.selectedLogs.clearSelectedLogIds);
|
||||||
|
|
||||||
|
const [selectedBaseModel, setSelectedBaseModel] = useState(SUPPORTED_BASE_MODELS[0]);
|
||||||
|
const [modelSlug, setModelSlug] = useState(humanId({ separator: "-", capitalize: false }));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disclosure.isOpen) {
|
||||||
|
setSelectedBaseModel(SUPPORTED_BASE_MODELS[0]);
|
||||||
|
setModelSlug(humanId({ separator: "-", capitalize: false }));
|
||||||
|
}
|
||||||
|
}, [disclosure.isOpen]);
|
||||||
|
|
||||||
|
const utils = api.useContext();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const createFineTuneMutation = api.fineTunes.create.useMutation();
|
||||||
|
|
||||||
|
const [createFineTune, creationInProgress] = useHandledAsyncCallback(async () => {
|
||||||
|
if (!selectedProjectId || !modelSlug || !selectedBaseModel || !selectedLogIds.size) return;
|
||||||
|
await createFineTuneMutation.mutateAsync({
|
||||||
|
projectId: selectedProjectId,
|
||||||
|
slug: modelSlug,
|
||||||
|
baseModel: selectedBaseModel,
|
||||||
|
selectedLogIds: Array.from(selectedLogIds),
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.fineTunes.list.invalidate();
|
||||||
|
await router.push({ pathname: "/fine-tunes" });
|
||||||
|
clearSelectedLogIds();
|
||||||
|
disclosure.onClose();
|
||||||
|
}, [createFineTuneMutation, selectedProjectId, selectedLogIds, modelSlug, selectedBaseModel]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal size={{ base: "xl", md: "2xl" }} {...disclosure}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent w={1200}>
|
||||||
|
<ModalHeader>
|
||||||
|
<HStack>
|
||||||
|
<Icon as={FaRobot} />
|
||||||
|
<Text>Fine Tune</Text>
|
||||||
|
</HStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody maxW="unset">
|
||||||
|
<VStack w="full" spacing={8} pt={4} alignItems="flex-start">
|
||||||
|
<Text>
|
||||||
|
We'll train on the <b>{selectedLogIds.size}</b> logs you've selected.
|
||||||
|
</Text>
|
||||||
|
<VStack>
|
||||||
|
<HStack spacing={2} w="full">
|
||||||
|
<Text fontWeight="bold" w={36}>
|
||||||
|
Model ID:
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
value={modelSlug}
|
||||||
|
onChange={(e) => setModelSlug(e.target.value)}
|
||||||
|
w={48}
|
||||||
|
placeholder="unique-id"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
// If the user types anything other than a-z, A-Z, or 0-9, replace it with -
|
||||||
|
if (!/[a-zA-Z0-9]/.test(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
setModelSlug((s) => s && `${s}-`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Text fontWeight="bold" w={36}>
|
||||||
|
Base model:
|
||||||
|
</Text>
|
||||||
|
<InputDropdown
|
||||||
|
options={SUPPORTED_BASE_MODELS}
|
||||||
|
selectedOption={selectedBaseModel}
|
||||||
|
onSelect={(option) => setSelectedBaseModel(option)}
|
||||||
|
inputGroupProps={{ w: 48 }}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
<Button variant="unstyled" color="blue.600">
|
||||||
|
<HStack>
|
||||||
|
<Text>Advanced Options</Text>
|
||||||
|
<Icon as={FiChevronDown} />
|
||||||
|
</HStack>
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<HStack>
|
||||||
|
<Button colorScheme="gray" onClick={disclosure.onClose} minW={24}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
onClick={createFineTune}
|
||||||
|
isLoading={creationInProgress}
|
||||||
|
minW={24}
|
||||||
|
isDisabled={!modelSlug}
|
||||||
|
>
|
||||||
|
Start Training
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
55
app/src/components/requestLogs/FormattedJson.tsx
Normal file
55
app/src/components/requestLogs/FormattedJson.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Box, IconButton, useToast } from "@chakra-ui/react";
|
||||||
|
import { CopyIcon } from "lucide-react";
|
||||||
|
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||||
|
import { atelierCaveLight } from "react-syntax-highlighter/dist/cjs/styles/hljs";
|
||||||
|
import stringify from "json-stringify-pretty-compact";
|
||||||
|
|
||||||
|
const FormattedJson = ({ json }: { json: any }) => {
|
||||||
|
const jsonString = stringify(json, { maxLength: 40 });
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const copyToClipboard = async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
toast({
|
||||||
|
title: "Copied to clipboard",
|
||||||
|
status: "success",
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
toast({
|
||||||
|
title: "Failed to copy to clipboard",
|
||||||
|
status: "error",
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box position="relative" fontSize="sm" borderRadius="md" overflow="hidden">
|
||||||
|
<SyntaxHighlighter
|
||||||
|
customStyle={{ overflowX: "unset" }}
|
||||||
|
language="json"
|
||||||
|
style={atelierCaveLight}
|
||||||
|
lineProps={{
|
||||||
|
style: { wordBreak: "break-all", whiteSpace: "pre-wrap" },
|
||||||
|
}}
|
||||||
|
wrapLines
|
||||||
|
>
|
||||||
|
{jsonString}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Copy"
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
position="absolute"
|
||||||
|
top={1}
|
||||||
|
right={1}
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => void copyToClipboard(jsonString)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { FormattedJson };
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Button, HStack, Icon, Text } from "@chakra-ui/react";
|
||||||
|
import { BsPlus } from "react-icons/bs";
|
||||||
|
import { comparators, defaultFilterableFields } from "~/state/logFiltersSlice";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
|
||||||
|
const AddFilterButton = () => {
|
||||||
|
const addFilter = useAppStore((s) => s.logFilters.addFilter);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack
|
||||||
|
as={Button}
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
addFilter({
|
||||||
|
id: Date.now().toString(),
|
||||||
|
field: defaultFilterableFields[0],
|
||||||
|
comparator: comparators[0],
|
||||||
|
value: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
spacing={0}
|
||||||
|
fontSize="sm"
|
||||||
|
>
|
||||||
|
<Icon as={BsPlus} boxSize={5} />
|
||||||
|
<Text>Add Filter</Text>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddFilterButton;
|
||||||
44
app/src/components/requestLogs/LogFilters/LogFilter.tsx
Normal file
44
app/src/components/requestLogs/LogFilters/LogFilter.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { HStack, IconButton, Input } from "@chakra-ui/react";
|
||||||
|
import { BsTrash } from "react-icons/bs";
|
||||||
|
|
||||||
|
import { type LogFilter } from "~/state/logFiltersSlice";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import { debounce } from "lodash-es";
|
||||||
|
import SelectFieldDropdown from "./SelectFieldDropdown";
|
||||||
|
import SelectComparatorDropdown from "./SelectComparatorDropdown";
|
||||||
|
|
||||||
|
const LogFilter = ({ filter }: { filter: LogFilter }) => {
|
||||||
|
const updateFilter = useAppStore((s) => s.logFilters.updateFilter);
|
||||||
|
const deleteFilter = useAppStore((s) => s.logFilters.deleteFilter);
|
||||||
|
|
||||||
|
const [editedValue, setEditedValue] = useState(filter.value);
|
||||||
|
|
||||||
|
const debouncedUpdateFilter = useCallback(
|
||||||
|
debounce((filter: LogFilter) => updateFilter(filter), 500, {
|
||||||
|
leading: true,
|
||||||
|
}),
|
||||||
|
[updateFilter],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HStack>
|
||||||
|
<SelectFieldDropdown filter={filter} />
|
||||||
|
<SelectComparatorDropdown filter={filter} />
|
||||||
|
<Input
|
||||||
|
value={editedValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEditedValue(e.target.value);
|
||||||
|
debouncedUpdateFilter({ ...filter, value: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Delete Filter"
|
||||||
|
icon={<BsTrash />}
|
||||||
|
onClick={() => deleteFilter(filter.id)}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogFilter;
|
||||||
30
app/src/components/requestLogs/LogFilters/LogFilters.tsx
Normal file
30
app/src/components/requestLogs/LogFilters/LogFilters.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { VStack, Text } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import AddFilterButton from "./AddFilterButton";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import LogFilter from "./LogFilter";
|
||||||
|
|
||||||
|
const LogFilters = () => {
|
||||||
|
const filters = useAppStore((s) => s.logFilters.filters);
|
||||||
|
return (
|
||||||
|
<VStack
|
||||||
|
bgColor="white"
|
||||||
|
borderRadius={8}
|
||||||
|
borderWidth={1}
|
||||||
|
w="full"
|
||||||
|
alignItems="flex-start"
|
||||||
|
p={4}
|
||||||
|
spacing={4}
|
||||||
|
>
|
||||||
|
<Text fontWeight="bold" color="gray.500">
|
||||||
|
Filters
|
||||||
|
</Text>
|
||||||
|
{filters.map((filter) => (
|
||||||
|
<LogFilter key={filter.id} filter={filter} />
|
||||||
|
))}
|
||||||
|
<AddFilterButton />
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogFilters;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { comparators, type LogFilter } from "~/state/logFiltersSlice";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import InputDropdown from "~/components/InputDropdown";
|
||||||
|
|
||||||
|
const SelectComparatorDropdown = ({ filter }: { filter: LogFilter }) => {
|
||||||
|
const updateFilter = useAppStore((s) => s.logFilters.updateFilter);
|
||||||
|
|
||||||
|
const { comparator } = filter;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputDropdown
|
||||||
|
options={comparators}
|
||||||
|
selectedOption={comparator}
|
||||||
|
onSelect={(option) => updateFilter({ ...filter, comparator: option })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectComparatorDropdown;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defaultFilterableFields, type LogFilter } from "~/state/logFiltersSlice";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import { useTagNames } from "~/utils/hooks";
|
||||||
|
import InputDropdown from "~/components/InputDropdown";
|
||||||
|
|
||||||
|
const SelectFieldDropdown = ({ filter }: { filter: LogFilter }) => {
|
||||||
|
const tagNames = useTagNames().data;
|
||||||
|
|
||||||
|
const updateFilter = useAppStore((s) => s.logFilters.updateFilter);
|
||||||
|
|
||||||
|
const { field } = filter;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputDropdown
|
||||||
|
options={[...defaultFilterableFields, ...(tagNames || [])]}
|
||||||
|
selectedOption={field}
|
||||||
|
onSelect={(option) => updateFilter({ ...filter, field: option })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SelectFieldDropdown;
|
||||||
16
app/src/components/requestLogs/LoggedCallsPaginator.tsx
Normal file
16
app/src/components/requestLogs/LoggedCallsPaginator.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { type StackProps } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { useLoggedCalls } from "~/utils/hooks";
|
||||||
|
import Paginator from "../Paginator";
|
||||||
|
|
||||||
|
const LoggedCallsPaginator = (props: StackProps) => {
|
||||||
|
const { data } = useLoggedCalls();
|
||||||
|
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const { count } = data;
|
||||||
|
|
||||||
|
return <Paginator count={count} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoggedCallsPaginator;
|
||||||
36
app/src/components/requestLogs/LoggedCallsTable.tsx
Normal file
36
app/src/components/requestLogs/LoggedCallsTable.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Card, Table, Tbody } from "@chakra-ui/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useLoggedCalls } from "~/utils/hooks";
|
||||||
|
import { TableHeader, TableRow } from "./TableRow";
|
||||||
|
|
||||||
|
export default function LoggedCallsTable() {
|
||||||
|
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||||
|
const loggedCalls = useLoggedCalls().data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card width="100%" overflowX="auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader showOptions />
|
||||||
|
<Tbody>
|
||||||
|
{loggedCalls?.calls?.map((loggedCall) => {
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
key={loggedCall.id}
|
||||||
|
loggedCall={loggedCall}
|
||||||
|
isExpanded={loggedCall.id === expandedRow}
|
||||||
|
onToggle={() => {
|
||||||
|
if (loggedCall.id === expandedRow) {
|
||||||
|
setExpandedRow(null);
|
||||||
|
} else {
|
||||||
|
setExpandedRow(loggedCall.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
showOptions
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
200
app/src/components/requestLogs/TableRow.tsx
Normal file
200
app/src/components/requestLogs/TableRow.tsx
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Heading,
|
||||||
|
Td,
|
||||||
|
Tr,
|
||||||
|
Thead,
|
||||||
|
Th,
|
||||||
|
Tooltip,
|
||||||
|
Collapse,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Text,
|
||||||
|
Checkbox,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import dayjs from "~/utils/dayjs";
|
||||||
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
|
import { FormattedJson } from "./FormattedJson";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import { useIsClientRehydrated, useLoggedCalls, useTagNames } from "~/utils/hooks";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { StaticColumnKeys } from "~/state/columnVisiblitySlice";
|
||||||
|
|
||||||
|
type LoggedCall = RouterOutputs["loggedCalls"]["list"]["calls"][0];
|
||||||
|
|
||||||
|
export const TableHeader = ({ showOptions }: { showOptions?: boolean }) => {
|
||||||
|
const matchingLogIds = useLoggedCalls().data?.matchingLogIds;
|
||||||
|
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
||||||
|
const addAll = useAppStore((s) => s.selectedLogs.addSelectedLogIds);
|
||||||
|
const clearAll = useAppStore((s) => s.selectedLogs.clearSelectedLogIds);
|
||||||
|
const allSelected = useMemo(() => {
|
||||||
|
if (!matchingLogIds || !matchingLogIds.length) return false;
|
||||||
|
return matchingLogIds.every((id) => selectedLogIds.has(id));
|
||||||
|
}, [selectedLogIds, matchingLogIds]);
|
||||||
|
const tagNames = useTagNames().data;
|
||||||
|
const visibleColumns = useAppStore((s) => s.columnVisibility.visibleColumns);
|
||||||
|
const isClientRehydrated = useIsClientRehydrated();
|
||||||
|
if (!isClientRehydrated) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
{showOptions && (
|
||||||
|
<Th pr={0}>
|
||||||
|
<HStack minW={16}>
|
||||||
|
<Checkbox
|
||||||
|
isChecked={allSelected}
|
||||||
|
onChange={() => {
|
||||||
|
allSelected ? clearAll() : addAll(matchingLogIds || []);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text>
|
||||||
|
({selectedLogIds.size ? `${selectedLogIds.size}/` : ""}
|
||||||
|
{matchingLogIds?.length || 0})
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Th>
|
||||||
|
)}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.SENT_AT) && <Th>Sent At</Th>}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.MODEL) && <Th>Model</Th>}
|
||||||
|
{tagNames
|
||||||
|
?.filter((tagName) => visibleColumns.has(tagName))
|
||||||
|
.map((tagName) => (
|
||||||
|
<Th key={tagName} textTransform={"none"}>
|
||||||
|
{tagName}
|
||||||
|
</Th>
|
||||||
|
))}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.DURATION) && <Th isNumeric>Duration</Th>}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.INPUT_TOKENS) && <Th isNumeric>Input tokens</Th>}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.OUTPUT_TOKENS) && <Th isNumeric>Output tokens</Th>}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.STATUS_CODE) && <Th isNumeric>Status</Th>}
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableRow = ({
|
||||||
|
loggedCall,
|
||||||
|
isExpanded,
|
||||||
|
onToggle,
|
||||||
|
showOptions,
|
||||||
|
}: {
|
||||||
|
loggedCall: LoggedCall;
|
||||||
|
isExpanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
showOptions?: boolean;
|
||||||
|
}) => {
|
||||||
|
const isError = loggedCall.modelResponse?.statusCode !== 200;
|
||||||
|
const requestedAt = dayjs(loggedCall.requestedAt).format("MMMM D h:mm A");
|
||||||
|
const fullTime = dayjs(loggedCall.requestedAt).toString();
|
||||||
|
|
||||||
|
const isChecked = useAppStore((s) => s.selectedLogs.selectedLogIds.has(loggedCall.id));
|
||||||
|
const toggleChecked = useAppStore((s) => s.selectedLogs.toggleSelectedLogId);
|
||||||
|
|
||||||
|
const tagNames = useTagNames().data;
|
||||||
|
const visibleColumns = useAppStore((s) => s.columnVisibility.visibleColumns);
|
||||||
|
|
||||||
|
const visibleTagNames = useMemo(() => {
|
||||||
|
return tagNames?.filter((tagName) => visibleColumns.has(tagName)) ?? [];
|
||||||
|
}, [tagNames, visibleColumns]);
|
||||||
|
|
||||||
|
const isClientRehydrated = useIsClientRehydrated();
|
||||||
|
if (!isClientRehydrated) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr
|
||||||
|
onClick={onToggle}
|
||||||
|
key={loggedCall.id}
|
||||||
|
_hover={{ bgColor: "gray.50", cursor: "pointer" }}
|
||||||
|
sx={{
|
||||||
|
"> td": { borderBottom: "none" },
|
||||||
|
}}
|
||||||
|
fontSize="sm"
|
||||||
|
>
|
||||||
|
{showOptions && (
|
||||||
|
<Td>
|
||||||
|
<Checkbox isChecked={isChecked} onChange={() => toggleChecked(loggedCall.id)} />
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.SENT_AT) && (
|
||||||
|
<Td>
|
||||||
|
<Tooltip label={fullTime} placement="top">
|
||||||
|
<Box whiteSpace="nowrap" minW="120px">
|
||||||
|
{requestedAt}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.MODEL) && (
|
||||||
|
<Td>
|
||||||
|
<HStack justifyContent="flex-start">
|
||||||
|
<Text
|
||||||
|
colorScheme="purple"
|
||||||
|
color="purple.500"
|
||||||
|
borderColor="purple.500"
|
||||||
|
px={1}
|
||||||
|
borderRadius={4}
|
||||||
|
borderWidth={1}
|
||||||
|
fontSize="xs"
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
>
|
||||||
|
{loggedCall.model}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
{visibleTagNames.map((tagName) => (
|
||||||
|
<Td key={tagName}>{loggedCall.tags[tagName]}</Td>
|
||||||
|
))}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.DURATION) && (
|
||||||
|
<Td isNumeric>
|
||||||
|
{loggedCall.cacheHit ? (
|
||||||
|
<Text color="gray.500">Cached</Text>
|
||||||
|
) : (
|
||||||
|
((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s"
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.INPUT_TOKENS) && (
|
||||||
|
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
|
||||||
|
)}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.OUTPUT_TOKENS) && (
|
||||||
|
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
|
||||||
|
)}
|
||||||
|
{visibleColumns.has(StaticColumnKeys.STATUS_CODE) && (
|
||||||
|
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
|
||||||
|
{loggedCall.modelResponse?.statusCode ?? "No response"}
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
</Tr>
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={visibleColumns.size + 1} w="full" p={0}>
|
||||||
|
<Collapse in={isExpanded} unmountOnExit={true}>
|
||||||
|
<VStack p={4} align="stretch">
|
||||||
|
<HStack align="stretch">
|
||||||
|
<VStack flex={1} align="stretch">
|
||||||
|
<Heading size="sm">Input</Heading>
|
||||||
|
<FormattedJson json={loggedCall.modelResponse?.reqPayload} />
|
||||||
|
</VStack>
|
||||||
|
<VStack flex={1} align="stretch">
|
||||||
|
<Heading size="sm">Output</Heading>
|
||||||
|
<FormattedJson json={loggedCall.modelResponse?.respPayload} />
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
<ButtonGroup alignSelf="flex-end">
|
||||||
|
<Button as={Link} colorScheme="blue" href={{ pathname: "/experiments" }}>
|
||||||
|
Experiments
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</VStack>
|
||||||
|
</Collapse>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -21,6 +21,19 @@ export const env = createEnv({
|
|||||||
ANTHROPIC_API_KEY: z.string().default("placeholder"),
|
ANTHROPIC_API_KEY: z.string().default("placeholder"),
|
||||||
SENTRY_AUTH_TOKEN: z.string().optional(),
|
SENTRY_AUTH_TOKEN: z.string().optional(),
|
||||||
OPENPIPE_API_KEY: z.string().optional(),
|
OPENPIPE_API_KEY: z.string().optional(),
|
||||||
|
SENDER_EMAIL: z.string().default("placeholder"),
|
||||||
|
SMTP_HOST: z.string().default("placeholder"),
|
||||||
|
SMTP_PORT: z.string().default("placeholder"),
|
||||||
|
SMTP_LOGIN: z.string().default("placeholder"),
|
||||||
|
SMTP_PASSWORD: z.string().default("placeholder"),
|
||||||
|
WORKER_CONCURRENCY: z
|
||||||
|
.string()
|
||||||
|
.default("10")
|
||||||
|
.transform((val) => parseInt(val)),
|
||||||
|
WORKER_MAX_POOL_SIZE: z
|
||||||
|
.string()
|
||||||
|
.default("10")
|
||||||
|
.transform((val) => parseInt(val)),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,8 +46,6 @@ export const env = createEnv({
|
|||||||
NEXT_PUBLIC_SOCKET_URL: z.string().url().default("http://localhost:3318"),
|
NEXT_PUBLIC_SOCKET_URL: z.string().url().default("http://localhost:3318"),
|
||||||
NEXT_PUBLIC_HOST: z.string().url().default("http://localhost:3000"),
|
NEXT_PUBLIC_HOST: z.string().url().default("http://localhost:3000"),
|
||||||
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
|
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
|
||||||
NEXT_PUBLIC_SHOW_DATA: z.string().optional(),
|
|
||||||
NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS: z.string().optional(),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +60,6 @@ export const env = createEnv({
|
|||||||
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
|
||||||
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
|
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
|
||||||
NEXT_PUBLIC_HOST: process.env.NEXT_PUBLIC_HOST,
|
NEXT_PUBLIC_HOST: process.env.NEXT_PUBLIC_HOST,
|
||||||
NEXT_PUBLIC_SHOW_DATA: process.env.NEXT_PUBLIC_SHOW_DATA,
|
|
||||||
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
||||||
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
||||||
REPLICATE_API_TOKEN: process.env.REPLICATE_API_TOKEN,
|
REPLICATE_API_TOKEN: process.env.REPLICATE_API_TOKEN,
|
||||||
@@ -57,7 +67,13 @@ export const env = createEnv({
|
|||||||
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||||
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
|
||||||
OPENPIPE_API_KEY: process.env.OPENPIPE_API_KEY,
|
OPENPIPE_API_KEY: process.env.OPENPIPE_API_KEY,
|
||||||
NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS: process.env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS,
|
SENDER_EMAIL: process.env.SENDER_EMAIL,
|
||||||
|
SMTP_HOST: process.env.SMTP_HOST,
|
||||||
|
SMTP_PORT: process.env.SMTP_PORT,
|
||||||
|
SMTP_LOGIN: process.env.SMTP_LOGIN,
|
||||||
|
SMTP_PASSWORD: process.env.SMTP_PASSWORD,
|
||||||
|
WORKER_CONCURRENCY: process.env.WORKER_CONCURRENCY,
|
||||||
|
WORKER_MAX_POOL_SIZE: process.env.WORKER_MAX_POOL_SIZE,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import openaiChatCompletionFrontend from "./openai-ChatCompletion/frontend";
|
import openaiChatCompletionFrontend from "./openai-ChatCompletion/frontend";
|
||||||
import replicateLlama2Frontend from "./replicate-llama2/frontend";
|
import replicateLlama2Frontend from "./replicate-llama2/frontend";
|
||||||
import anthropicFrontend from "./anthropic-completion/frontend";
|
import anthropicFrontend from "./anthropic-completion/frontend";
|
||||||
|
import openpipeFrontend from "./openpipe-chat/frontend";
|
||||||
import { type SupportedProvider, type FrontendModelProvider } from "./types";
|
import { type SupportedProvider, type FrontendModelProvider } from "./types";
|
||||||
|
|
||||||
// Keep attributes here that need to be accessible from the frontend. We can't
|
// Keep attributes here that need to be accessible from the frontend. We can't
|
||||||
@@ -10,6 +11,7 @@ const frontendModelProviders: Record<SupportedProvider, FrontendModelProvider<an
|
|||||||
"openai/ChatCompletion": openaiChatCompletionFrontend,
|
"openai/ChatCompletion": openaiChatCompletionFrontend,
|
||||||
"replicate/llama2": replicateLlama2Frontend,
|
"replicate/llama2": replicateLlama2Frontend,
|
||||||
"anthropic/completion": anthropicFrontend,
|
"anthropic/completion": anthropicFrontend,
|
||||||
|
"openpipe/Chat": openpipeFrontend,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default frontendModelProviders;
|
export default frontendModelProviders;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import openaiChatCompletion from "./openai-ChatCompletion";
|
import openaiChatCompletion from "./openai-ChatCompletion";
|
||||||
import replicateLlama2 from "./replicate-llama2";
|
import replicateLlama2 from "./replicate-llama2";
|
||||||
import anthropicCompletion from "./anthropic-completion";
|
import anthropicCompletion from "./anthropic-completion";
|
||||||
|
import openpipeChatCompletion from "./openpipe-chat";
|
||||||
import { type SupportedProvider, type ModelProvider } from "./types";
|
import { type SupportedProvider, type ModelProvider } from "./types";
|
||||||
|
|
||||||
const modelProviders: Record<SupportedProvider, ModelProvider<any, any, any>> = {
|
const modelProviders: Record<SupportedProvider, ModelProvider<any, any, any>> = {
|
||||||
"openai/ChatCompletion": openaiChatCompletion,
|
"openai/ChatCompletion": openaiChatCompletion,
|
||||||
"replicate/llama2": replicateLlama2,
|
"replicate/llama2": replicateLlama2,
|
||||||
"anthropic/completion": anthropicCompletion,
|
"anthropic/completion": anthropicCompletion,
|
||||||
|
"openpipe/Chat": openpipeChatCompletion,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default modelProviders;
|
export default modelProviders;
|
||||||
|
|||||||
@@ -1,54 +1,10 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
import {
|
import { isArray, isString } from "lodash-es";
|
||||||
type ChatCompletionChunk,
|
|
||||||
type ChatCompletion,
|
|
||||||
type CompletionCreateParams,
|
|
||||||
} from "openai/resources/chat";
|
|
||||||
import { type CompletionResponse } from "../types";
|
|
||||||
import { isArray, isString, omit } from "lodash-es";
|
|
||||||
import { openai } from "~/server/utils/openai";
|
|
||||||
import { APIError } from "openai";
|
import { APIError } from "openai";
|
||||||
|
import { type ChatCompletion, type CompletionCreateParams } from "openai/resources/chat";
|
||||||
const mergeStreamedChunks = (
|
import mergeChunks from "openpipe/src/openai/mergeChunks";
|
||||||
base: ChatCompletion | null,
|
import { openai } from "~/server/utils/openai";
|
||||||
chunk: ChatCompletionChunk,
|
import { type CompletionResponse } from "../types";
|
||||||
): ChatCompletion => {
|
|
||||||
if (base === null) {
|
|
||||||
return mergeStreamedChunks({ ...chunk, choices: [] }, chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
const choices = [...base.choices];
|
|
||||||
for (const choice of chunk.choices) {
|
|
||||||
const baseChoice = choices.find((c) => c.index === choice.index);
|
|
||||||
if (baseChoice) {
|
|
||||||
baseChoice.finish_reason = choice.finish_reason ?? baseChoice.finish_reason;
|
|
||||||
baseChoice.message = baseChoice.message ?? { role: "assistant" };
|
|
||||||
|
|
||||||
if (choice.delta?.content)
|
|
||||||
baseChoice.message.content =
|
|
||||||
((baseChoice.message.content as string) ?? "") + (choice.delta.content ?? "");
|
|
||||||
if (choice.delta?.function_call) {
|
|
||||||
const fnCall = baseChoice.message.function_call ?? {};
|
|
||||||
fnCall.name =
|
|
||||||
((fnCall.name as string) ?? "") + ((choice.delta.function_call.name as string) ?? "");
|
|
||||||
fnCall.arguments =
|
|
||||||
((fnCall.arguments as string) ?? "") +
|
|
||||||
((choice.delta.function_call.arguments as string) ?? "");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// @ts-expect-error the types are correctly telling us that finish_reason
|
|
||||||
// could be null, but don't want to fix it right now.
|
|
||||||
choices.push({ ...omit(choice, "delta"), message: { role: "assistant", ...choice.delta } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const merged: ChatCompletion = {
|
|
||||||
...base,
|
|
||||||
choices,
|
|
||||||
};
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getCompletion(
|
export async function getCompletion(
|
||||||
input: CompletionCreateParams,
|
input: CompletionCreateParams,
|
||||||
@@ -59,19 +15,25 @@ export async function getCompletion(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (onStream) {
|
if (onStream) {
|
||||||
console.log("got started");
|
|
||||||
const resp = await openai.chat.completions.create(
|
const resp = await openai.chat.completions.create(
|
||||||
{ ...input, stream: true },
|
{
|
||||||
|
...input,
|
||||||
|
stream: true,
|
||||||
|
openpipe: {
|
||||||
|
tags: {
|
||||||
|
prompt_id: "getCompletion",
|
||||||
|
stream: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
maxRetries: 0,
|
maxRetries: 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
for await (const part of resp) {
|
for await (const part of resp) {
|
||||||
console.log("got part", part);
|
finalCompletion = mergeChunks(finalCompletion, part);
|
||||||
finalCompletion = mergeStreamedChunks(finalCompletion, part);
|
|
||||||
onStream(finalCompletion);
|
onStream(finalCompletion);
|
||||||
}
|
}
|
||||||
console.log("got final", finalCompletion);
|
|
||||||
if (!finalCompletion) {
|
if (!finalCompletion) {
|
||||||
return {
|
return {
|
||||||
type: "error",
|
type: "error",
|
||||||
@@ -81,7 +43,16 @@ export async function getCompletion(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const resp = await openai.chat.completions.create(
|
const resp = await openai.chat.completions.create(
|
||||||
{ ...input, stream: false },
|
{
|
||||||
|
...input,
|
||||||
|
stream: false,
|
||||||
|
openpipe: {
|
||||||
|
tags: {
|
||||||
|
prompt_id: "getCompletion",
|
||||||
|
stream: "false",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
maxRetries: 0,
|
maxRetries: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-4",
|
model: "gpt-4",
|
||||||
stream: true,
|
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -29,7 +28,6 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-4",
|
model: "gpt-4",
|
||||||
stream: true,
|
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -120,13 +118,12 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
"Convert to function call": {
|
"Convert to function call": {
|
||||||
icon: TfiThought,
|
icon: TfiThought,
|
||||||
description: "Use function calls to get output from the model in a more structured way.",
|
description: "Use function calls to get output from the model in a more structured way.",
|
||||||
instructions: `OpenAI functions are a specialized way for an LLM to return output.
|
instructions: `OpenAI functions are a specialized way for an LLM to return its final output.
|
||||||
|
|
||||||
This is what a prompt looks like before adding a function:
|
Example 1 before:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-4",
|
model: "gpt-4",
|
||||||
stream: true,
|
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -139,11 +136,10 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
This is what one looks like after adding a function:
|
Example 1 after:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-4",
|
model: "gpt-4",
|
||||||
stream: true,
|
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -156,7 +152,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
functions: [
|
functions: [
|
||||||
{
|
{
|
||||||
name: "extract_sentiment",
|
name: "log_extracted_sentiment",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object", // parameters must always be an object with a properties key
|
type: "object", // parameters must always be an object with a properties key
|
||||||
properties: { // properties key is required
|
properties: { // properties key is required
|
||||||
@@ -169,13 +165,13 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
function_call: {
|
function_call: {
|
||||||
name: "extract_sentiment",
|
name: "log_extracted_sentiment",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Here's another example of adding a function:
|
=========
|
||||||
|
|
||||||
Before:
|
Example 2 before:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
@@ -197,7 +193,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
temperature: 0,
|
temperature: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
After:
|
Example 2 after:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
@@ -215,7 +211,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
temperature: 0,
|
temperature: 0,
|
||||||
functions: [
|
functions: [
|
||||||
{
|
{
|
||||||
name: "score_post",
|
name: "log_post_score",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
@@ -227,17 +223,16 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
function_call: {
|
function_call: {
|
||||||
name: "score_post",
|
name: "log_post_score",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Another example
|
=========
|
||||||
|
|
||||||
Before:
|
Example 3 before:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
stream: true,
|
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
@@ -246,7 +241,7 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
After:
|
Example 3 after:
|
||||||
|
|
||||||
definePrompt("openai/ChatCompletion", {
|
definePrompt("openai/ChatCompletion", {
|
||||||
model: "gpt-3.5-turbo",
|
model: "gpt-3.5-turbo",
|
||||||
@@ -258,22 +253,25 @@ export const refinementActions: Record<string, RefinementAction> = {
|
|||||||
],
|
],
|
||||||
functions: [
|
functions: [
|
||||||
{
|
{
|
||||||
name: "write_in_language",
|
name: "log_translated_text",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
text: {
|
translated_text: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
description: "The text, written in the language specified in the prompt",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
function_call: {
|
function_call: {
|
||||||
name: "write_in_language",
|
name: "log_translated_text",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
=========
|
||||||
|
|
||||||
Add an OpenAI function that takes one or more nested parameters that match the expected output from this prompt.`,
|
Add an OpenAI function that takes one or more nested parameters that match the expected output from this prompt.`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
98
app/src/modelProviders/openpipe-chat/frontend.ts
Normal file
98
app/src/modelProviders/openpipe-chat/frontend.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { type OpenpipeChatOutput, type SupportedModel } from ".";
|
||||||
|
import { type FrontendModelProvider } from "../types";
|
||||||
|
import { refinementActions } from "./refinementActions";
|
||||||
|
import {
|
||||||
|
templateOpenOrcaPrompt,
|
||||||
|
templateAlpacaInstructPrompt,
|
||||||
|
// templateSystemUserAssistantPrompt,
|
||||||
|
templateInstructionInputResponsePrompt,
|
||||||
|
templateAiroborosPrompt,
|
||||||
|
templateGryphePrompt,
|
||||||
|
templateVicunaPrompt,
|
||||||
|
} from "./templatePrompt";
|
||||||
|
|
||||||
|
const frontendModelProvider: FrontendModelProvider<SupportedModel, OpenpipeChatOutput> = {
|
||||||
|
name: "OpenAI ChatCompletion",
|
||||||
|
|
||||||
|
models: {
|
||||||
|
"Open-Orca/OpenOrcaxOpenChat-Preview2-13B": {
|
||||||
|
name: "OpenOrcaxOpenChat-Preview2-13B",
|
||||||
|
contextWindow: 4096,
|
||||||
|
pricePerSecond: 0.0003,
|
||||||
|
speed: "medium",
|
||||||
|
provider: "openpipe/Chat",
|
||||||
|
learnMoreUrl: "https://huggingface.co/Open-Orca/OpenOrcaxOpenChat-Preview2-13B",
|
||||||
|
templatePrompt: templateOpenOrcaPrompt,
|
||||||
|
},
|
||||||
|
"Open-Orca/OpenOrca-Platypus2-13B": {
|
||||||
|
name: "OpenOrca-Platypus2-13B",
|
||||||
|
contextWindow: 4096,
|
||||||
|
pricePerSecond: 0.0003,
|
||||||
|
speed: "medium",
|
||||||
|
provider: "openpipe/Chat",
|
||||||
|
learnMoreUrl: "https://huggingface.co/Open-Orca/OpenOrca-Platypus2-13B",
|
||||||
|
templatePrompt: templateAlpacaInstructPrompt,
|
||||||
|
defaultStopTokens: ["</s>"],
|
||||||
|
},
|
||||||
|
// "stabilityai/StableBeluga-13B": {
|
||||||
|
// name: "StableBeluga-13B",
|
||||||
|
// contextWindow: 4096,
|
||||||
|
// pricePerSecond: 0.0003,
|
||||||
|
// speed: "medium",
|
||||||
|
// provider: "openpipe/Chat",
|
||||||
|
// learnMoreUrl: "https://huggingface.co/stabilityai/StableBeluga-13B",
|
||||||
|
// templatePrompt: templateSystemUserAssistantPrompt,
|
||||||
|
// },
|
||||||
|
"NousResearch/Nous-Hermes-Llama2-13b": {
|
||||||
|
name: "Nous-Hermes-Llama2-13b",
|
||||||
|
contextWindow: 4096,
|
||||||
|
pricePerSecond: 0.0003,
|
||||||
|
speed: "medium",
|
||||||
|
provider: "openpipe/Chat",
|
||||||
|
learnMoreUrl: "https://huggingface.co/NousResearch/Nous-Hermes-Llama2-13b",
|
||||||
|
templatePrompt: templateInstructionInputResponsePrompt,
|
||||||
|
},
|
||||||
|
"jondurbin/airoboros-l2-13b-gpt4-2.0": {
|
||||||
|
name: "airoboros-l2-13b-gpt4-2.0",
|
||||||
|
contextWindow: 4096,
|
||||||
|
pricePerSecond: 0.0003,
|
||||||
|
speed: "medium",
|
||||||
|
provider: "openpipe/Chat",
|
||||||
|
learnMoreUrl: "https://huggingface.co/jondurbin/airoboros-l2-13b-gpt4-2.0",
|
||||||
|
templatePrompt: templateAiroborosPrompt,
|
||||||
|
},
|
||||||
|
"lmsys/vicuna-13b-v1.5": {
|
||||||
|
name: "vicuna-13b-v1.5",
|
||||||
|
contextWindow: 4096,
|
||||||
|
pricePerSecond: 0.0003,
|
||||||
|
speed: "medium",
|
||||||
|
provider: "openpipe/Chat",
|
||||||
|
learnMoreUrl: "https://huggingface.co/lmsys/vicuna-13b-v1.5",
|
||||||
|
templatePrompt: templateVicunaPrompt,
|
||||||
|
},
|
||||||
|
"Gryphe/MythoMax-L2-13b": {
|
||||||
|
name: "MythoMax-L2-13b",
|
||||||
|
contextWindow: 4096,
|
||||||
|
pricePerSecond: 0.0003,
|
||||||
|
speed: "medium",
|
||||||
|
provider: "openpipe/Chat",
|
||||||
|
learnMoreUrl: "https://huggingface.co/Gryphe/MythoMax-L2-13b",
|
||||||
|
templatePrompt: templateGryphePrompt,
|
||||||
|
},
|
||||||
|
"NousResearch/Nous-Hermes-llama-2-7b": {
|
||||||
|
name: "Nous-Hermes-llama-2-7b",
|
||||||
|
contextWindow: 4096,
|
||||||
|
pricePerSecond: 0.0003,
|
||||||
|
speed: "medium",
|
||||||
|
provider: "openpipe/Chat",
|
||||||
|
learnMoreUrl: "https://huggingface.co/NousResearch/Nous-Hermes-llama-2-7b",
|
||||||
|
templatePrompt: templateInstructionInputResponsePrompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
refinementActions,
|
||||||
|
|
||||||
|
normalizeOutput: (output) => ({ type: "text", value: output }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default frontendModelProvider;
|
||||||
121
app/src/modelProviders/openpipe-chat/getCompletion.ts
Normal file
121
app/src/modelProviders/openpipe-chat/getCompletion.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
import { isArray, isString } from "lodash-es";
|
||||||
|
import OpenAI, { APIError } from "openai";
|
||||||
|
|
||||||
|
import { type CompletionResponse } from "../types";
|
||||||
|
import { type OpenpipeChatInput, type OpenpipeChatOutput } from ".";
|
||||||
|
import frontendModelProvider from "./frontend";
|
||||||
|
|
||||||
|
const modelEndpoints: Record<OpenpipeChatInput["model"], string> = {
|
||||||
|
"Open-Orca/OpenOrcaxOpenChat-Preview2-13B": "https://5ef82gjxk8kdys-8000.proxy.runpod.net/v1",
|
||||||
|
"Open-Orca/OpenOrca-Platypus2-13B": "https://lt5qlel6qcji8t-8000.proxy.runpod.net/v1",
|
||||||
|
// "stabilityai/StableBeluga-13B": "https://vcorl8mxni2ou1-8000.proxy.runpod.net/v1",
|
||||||
|
"NousResearch/Nous-Hermes-Llama2-13b": "https://ncv8pw3u0vb8j2-8000.proxy.runpod.net/v1",
|
||||||
|
"jondurbin/airoboros-l2-13b-gpt4-2.0": "https://9nrbx7oph4btou-8000.proxy.runpod.net/v1",
|
||||||
|
"lmsys/vicuna-13b-v1.5": "https://h88hkt3ux73rb7-8000.proxy.runpod.net/v1",
|
||||||
|
"Gryphe/MythoMax-L2-13b": "https://3l5jvhnxdgky3v-8000.proxy.runpod.net/v1",
|
||||||
|
"NousResearch/Nous-Hermes-llama-2-7b": "https://ua1bpc6kv3dgge-8000.proxy.runpod.net/v1",
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getCompletion(
|
||||||
|
input: OpenpipeChatInput,
|
||||||
|
onStream: ((partialOutput: OpenpipeChatOutput) => void) | null,
|
||||||
|
): Promise<CompletionResponse<OpenpipeChatOutput>> {
|
||||||
|
const { model, messages, ...rest } = input;
|
||||||
|
|
||||||
|
const templatedPrompt = frontendModelProvider.models[model].templatePrompt?.(messages);
|
||||||
|
|
||||||
|
if (!templatedPrompt) {
|
||||||
|
return {
|
||||||
|
type: "error",
|
||||||
|
message: "Failed to generate prompt",
|
||||||
|
autoRetry: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
baseURL: modelEndpoints[model],
|
||||||
|
});
|
||||||
|
const start = Date.now();
|
||||||
|
let finalCompletion: OpenpipeChatOutput = "";
|
||||||
|
|
||||||
|
const completionParams = {
|
||||||
|
model,
|
||||||
|
prompt: templatedPrompt,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!completionParams.stop && frontendModelProvider.models[model].defaultStopTokens) {
|
||||||
|
completionParams.stop = frontendModelProvider.models[model].defaultStopTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (onStream) {
|
||||||
|
const resp = await openai.completions.create(
|
||||||
|
{ ...completionParams, stream: true },
|
||||||
|
{
|
||||||
|
maxRetries: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
for await (const part of resp) {
|
||||||
|
finalCompletion += part.choices[0]?.text;
|
||||||
|
onStream(finalCompletion);
|
||||||
|
}
|
||||||
|
if (!finalCompletion) {
|
||||||
|
return {
|
||||||
|
type: "error",
|
||||||
|
message: "Streaming failed to return a completion",
|
||||||
|
autoRetry: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const resp = await openai.completions.create(
|
||||||
|
{ ...completionParams, stream: false },
|
||||||
|
{
|
||||||
|
maxRetries: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
finalCompletion = resp.choices[0]?.text || "";
|
||||||
|
if (!finalCompletion) {
|
||||||
|
return {
|
||||||
|
type: "error",
|
||||||
|
message: "Failed to return a completion",
|
||||||
|
autoRetry: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const timeToComplete = Date.now() - start;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "success",
|
||||||
|
statusCode: 200,
|
||||||
|
value: finalCompletion,
|
||||||
|
timeToComplete,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof APIError) {
|
||||||
|
// The types from the sdk are wrong
|
||||||
|
const rawMessage = error.message as string | string[];
|
||||||
|
// If the message is not a string, stringify it
|
||||||
|
const message = isString(rawMessage)
|
||||||
|
? rawMessage
|
||||||
|
: isArray(rawMessage)
|
||||||
|
? rawMessage.map((m) => m.toString()).join("\n")
|
||||||
|
: (rawMessage as any).toString();
|
||||||
|
return {
|
||||||
|
type: "error",
|
||||||
|
message,
|
||||||
|
autoRetry: error.status === 429 || error.status === 503,
|
||||||
|
statusCode: error.status,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
return {
|
||||||
|
type: "error",
|
||||||
|
message: (error as Error).message,
|
||||||
|
autoRetry: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/src/modelProviders/openpipe-chat/index.ts
Normal file
54
app/src/modelProviders/openpipe-chat/index.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { type JSONSchema4 } from "json-schema";
|
||||||
|
import { type ModelProvider } from "../types";
|
||||||
|
import inputSchema from "./input.schema.json";
|
||||||
|
import { getCompletion } from "./getCompletion";
|
||||||
|
import frontendModelProvider from "./frontend";
|
||||||
|
|
||||||
|
const supportedModels = [
|
||||||
|
"Open-Orca/OpenOrcaxOpenChat-Preview2-13B",
|
||||||
|
"Open-Orca/OpenOrca-Platypus2-13B",
|
||||||
|
// "stabilityai/StableBeluga-13B",
|
||||||
|
"NousResearch/Nous-Hermes-Llama2-13b",
|
||||||
|
"jondurbin/airoboros-l2-13b-gpt4-2.0",
|
||||||
|
"lmsys/vicuna-13b-v1.5",
|
||||||
|
"Gryphe/MythoMax-L2-13b",
|
||||||
|
"NousResearch/Nous-Hermes-llama-2-7b",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type SupportedModel = (typeof supportedModels)[number];
|
||||||
|
|
||||||
|
export type OpenpipeChatInput = {
|
||||||
|
model: SupportedModel;
|
||||||
|
messages: {
|
||||||
|
role: "system" | "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
}[];
|
||||||
|
temperature?: number;
|
||||||
|
top_p?: number;
|
||||||
|
stop?: string[] | string;
|
||||||
|
max_tokens?: number;
|
||||||
|
presence_penalty?: number;
|
||||||
|
frequency_penalty?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OpenpipeChatOutput = string;
|
||||||
|
|
||||||
|
export type OpenpipeChatModelProvider = ModelProvider<
|
||||||
|
SupportedModel,
|
||||||
|
OpenpipeChatInput,
|
||||||
|
OpenpipeChatOutput
|
||||||
|
>;
|
||||||
|
|
||||||
|
const modelProvider: OpenpipeChatModelProvider = {
|
||||||
|
getModel: (input) => input.model,
|
||||||
|
inputSchema: inputSchema as JSONSchema4,
|
||||||
|
canStream: true,
|
||||||
|
getCompletion,
|
||||||
|
getUsage: (input, output) => {
|
||||||
|
// TODO: Implement this
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
...frontendModelProvider,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default modelProvider;
|
||||||
96
app/src/modelProviders/openpipe-chat/input.schema.json
Normal file
96
app/src/modelProviders/openpipe-chat/input.schema.json
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"model": {
|
||||||
|
"description": "ID of the model to use.",
|
||||||
|
"example": "Open-Orca/OpenOrcaxOpenChat-Preview2-13B",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Open-Orca/OpenOrcaxOpenChat-Preview2-13B",
|
||||||
|
"Open-Orca/OpenOrca-Platypus2-13B",
|
||||||
|
"NousResearch/Nous-Hermes-Llama2-13b",
|
||||||
|
"jondurbin/airoboros-l2-13b-gpt4-2.0",
|
||||||
|
"lmsys/vicuna-13b-v1.5",
|
||||||
|
"Gryphe/MythoMax-L2-13b",
|
||||||
|
"NousResearch/Nous-Hermes-llama-2-7b"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"description": "A list of messages comprising the conversation so far.",
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["system", "user", "assistant"],
|
||||||
|
"description": "The role of the messages author. One of `system`, `user`, or `assistant`."
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The contents of the message. `content` is required for all messages."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["role", "content"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 2,
|
||||||
|
"default": 1,
|
||||||
|
"example": 1,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\n\nWe generally recommend altering this or `top_p` but not both.\n"
|
||||||
|
},
|
||||||
|
"top_p": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 1,
|
||||||
|
"default": 1,
|
||||||
|
"example": 1,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\n\nWe generally recommend altering this or `temperature` but not both.\n"
|
||||||
|
},
|
||||||
|
"stop": {
|
||||||
|
"description": "Up to 4 sequences where the API will stop generating further tokens.\n",
|
||||||
|
"default": null,
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 4,
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"max_tokens": {
|
||||||
|
"description": "The maximum number of [tokens](/tokenizer) to generate in the chat completion.\n\nThe total length of input tokens and generated tokens is limited by the model's context length. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb) for counting tokens.\n",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"presence_penalty": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 0,
|
||||||
|
"minimum": -2,
|
||||||
|
"maximum": 2,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.\n\n[See more information about frequency and presence penalties.](/docs/api-reference/parameter-details)\n"
|
||||||
|
},
|
||||||
|
"frequency_penalty": {
|
||||||
|
"type": "number",
|
||||||
|
"default": 0,
|
||||||
|
"minimum": -2,
|
||||||
|
"maximum": 2,
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.\n\n[See more information about frequency and presence penalties.](/docs/api-reference/parameter-details)\n"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["model", "messages"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { type RefinementAction } from "../types";
|
||||||
|
|
||||||
|
export const refinementActions: Record<string, RefinementAction> = {};
|
||||||
274
app/src/modelProviders/openpipe-chat/templatePrompt.ts
Normal file
274
app/src/modelProviders/openpipe-chat/templatePrompt.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import { type OpenpipeChatInput } from ".";
|
||||||
|
|
||||||
|
// User: Hello<|end_of_turn|>Assistant: Hi<|end_of_turn|>User: How are you today?<|end_of_turn|>Assistant:
|
||||||
|
export const templateOpenOrcaPrompt = (messages: OpenpipeChatInput["messages"]) => {
|
||||||
|
const splitter = "<|end_of_turn|>";
|
||||||
|
|
||||||
|
const formattedMessages = messages.map((message) => {
|
||||||
|
if (message.role === "system" || message.role === "user") {
|
||||||
|
return "User: " + message.content;
|
||||||
|
} else {
|
||||||
|
return "Assistant: " + message.content;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let prompt = formattedMessages.join(splitter);
|
||||||
|
|
||||||
|
// Ensure that the prompt ends with an assistant message
|
||||||
|
const lastUserIndex = prompt.lastIndexOf("User:");
|
||||||
|
const lastAssistantIndex = prompt.lastIndexOf("Assistant:");
|
||||||
|
if (lastUserIndex > lastAssistantIndex) {
|
||||||
|
prompt += splitter + "Assistant:";
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ### Instruction:
|
||||||
|
|
||||||
|
// <prompt> (without the <>)
|
||||||
|
|
||||||
|
// ### Response: (leave two newlines for model to respond)
|
||||||
|
export const templateAlpacaInstructPrompt = (messages: OpenpipeChatInput["messages"]) => {
|
||||||
|
const splitter = "\n\n";
|
||||||
|
|
||||||
|
const userTag = "### Instruction:\n\n";
|
||||||
|
const assistantTag = "### Response:\n\n";
|
||||||
|
|
||||||
|
const formattedMessages = messages.map((message) => {
|
||||||
|
if (message.role === "system" || message.role === "user") {
|
||||||
|
return userTag + message.content;
|
||||||
|
} else {
|
||||||
|
return assistantTag + message.content;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let prompt = formattedMessages.join(splitter);
|
||||||
|
|
||||||
|
// Ensure that the prompt ends with an assistant message
|
||||||
|
const lastUserIndex = prompt.lastIndexOf(userTag);
|
||||||
|
const lastAssistantIndex = prompt.lastIndexOf(assistantTag);
|
||||||
|
if (lastUserIndex > lastAssistantIndex) {
|
||||||
|
prompt += splitter + assistantTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ### System:
|
||||||
|
// This is a system prompt, please behave and help the user.
|
||||||
|
|
||||||
|
// ### User:
|
||||||
|
// Your prompt here
|
||||||
|
|
||||||
|
// ### Assistant
|
||||||
|
// The output of Stable Beluga 13B
|
||||||
|
export const templateSystemUserAssistantPrompt = (messages: OpenpipeChatInput["messages"]) => {
|
||||||
|
const splitter = "\n\n";
|
||||||
|
|
||||||
|
const systemTag = "### System:\n";
|
||||||
|
const userTag = "### User:\n";
|
||||||
|
const assistantTag = "### Assistant\n";
|
||||||
|
|
||||||
|
const formattedMessages = messages.map((message) => {
|
||||||
|
if (message.role === "system") {
|
||||||
|
return systemTag + message.content;
|
||||||
|
} else if (message.role === "user") {
|
||||||
|
return userTag + message.content;
|
||||||
|
} else {
|
||||||
|
return assistantTag + message.content;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let prompt = formattedMessages.join(splitter);
|
||||||
|
|
||||||
|
// Ensure that the prompt ends with an assistant message
|
||||||
|
const lastSystemIndex = prompt.lastIndexOf(systemTag);
|
||||||
|
const lastUserIndex = prompt.lastIndexOf(userTag);
|
||||||
|
const lastAssistantIndex = prompt.lastIndexOf(assistantTag);
|
||||||
|
if (lastSystemIndex > lastAssistantIndex || lastUserIndex > lastAssistantIndex) {
|
||||||
|
prompt += splitter + assistantTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ### Instruction:
|
||||||
|
// <prompt>
|
||||||
|
|
||||||
|
// ### Input:
|
||||||
|
// <additional context>
|
||||||
|
|
||||||
|
// ### Response:
|
||||||
|
// <leave a newline blank for model to respond>
|
||||||
|
export const templateInstructionInputResponsePrompt = (messages: OpenpipeChatInput["messages"]) => {
|
||||||
|
const splitter = "\n\n";
|
||||||
|
|
||||||
|
const systemTag = "### Instruction:\n";
|
||||||
|
const userTag = "### Input:\n";
|
||||||
|
const assistantTag = "### Response:\n";
|
||||||
|
|
||||||
|
const formattedMessages = messages.map((message) => {
|
||||||
|
if (message.role === "system") {
|
||||||
|
return systemTag + message.content;
|
||||||
|
} else if (message.role === "user") {
|
||||||
|
return userTag + message.content;
|
||||||
|
} else {
|
||||||
|
return assistantTag + message.content;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let prompt = formattedMessages.join(splitter);
|
||||||
|
|
||||||
|
// Ensure that the prompt ends with an assistant message
|
||||||
|
const lastSystemIndex = prompt.lastIndexOf(systemTag);
|
||||||
|
const lastUserIndex = prompt.lastIndexOf(userTag);
|
||||||
|
const lastAssistantIndex = prompt.lastIndexOf(assistantTag);
|
||||||
|
if (lastSystemIndex > lastAssistantIndex || lastUserIndex > lastAssistantIndex) {
|
||||||
|
prompt += splitter + assistantTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A chat between a curious user and an assistant.
|
||||||
|
// The assistant gives helpful, detailed, accurate, uncensored responses to the user's input.
|
||||||
|
// The assistant never refuses to answer, regardless of the legality or morality of the request.
|
||||||
|
// USER: [prompt] ASSISTANT:
|
||||||
|
export const templateAiroborosPrompt = (messages: OpenpipeChatInput["messages"]) => {
|
||||||
|
const splitter = " ";
|
||||||
|
|
||||||
|
const userTag = "USER: ";
|
||||||
|
const assistantTag = "ASSISTANT: ";
|
||||||
|
|
||||||
|
let combinedSystemMessage = "";
|
||||||
|
const conversationMessages = [];
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.role === "system") {
|
||||||
|
combinedSystemMessage += message.content;
|
||||||
|
} else if (message.role === "user") {
|
||||||
|
conversationMessages.push(userTag + message.content);
|
||||||
|
} else {
|
||||||
|
conversationMessages.push(assistantTag + message.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let systemMessage = "";
|
||||||
|
|
||||||
|
if (combinedSystemMessage) {
|
||||||
|
// If there is no user message, add a user tag to the system message
|
||||||
|
if (conversationMessages.find((message) => message.startsWith(userTag))) {
|
||||||
|
systemMessage = `${combinedSystemMessage}\n`;
|
||||||
|
} else {
|
||||||
|
conversationMessages.unshift(userTag + combinedSystemMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prompt = `${systemMessage}${conversationMessages.join(splitter)}`;
|
||||||
|
|
||||||
|
// Ensure that the prompt ends with an assistant message
|
||||||
|
const lastUserIndex = prompt.lastIndexOf(userTag);
|
||||||
|
const lastAssistantIndex = prompt.lastIndexOf(assistantTag);
|
||||||
|
|
||||||
|
if (lastUserIndex > lastAssistantIndex) {
|
||||||
|
prompt += splitter + assistantTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
|
||||||
|
|
||||||
|
// USER: {prompt}
|
||||||
|
// ASSISTANT:
|
||||||
|
export const templateVicunaPrompt = (messages: OpenpipeChatInput["messages"]) => {
|
||||||
|
const splitter = "\n";
|
||||||
|
|
||||||
|
const humanTag = "USER: ";
|
||||||
|
const assistantTag = "ASSISTANT: ";
|
||||||
|
|
||||||
|
let combinedSystemMessage = "";
|
||||||
|
const conversationMessages = [];
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.role === "system") {
|
||||||
|
combinedSystemMessage += message.content;
|
||||||
|
} else if (message.role === "user") {
|
||||||
|
conversationMessages.push(humanTag + message.content);
|
||||||
|
} else {
|
||||||
|
conversationMessages.push(assistantTag + message.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let systemMessage = "";
|
||||||
|
|
||||||
|
if (combinedSystemMessage) {
|
||||||
|
// If there is no user message, add a user tag to the system message
|
||||||
|
if (conversationMessages.find((message) => message.startsWith(humanTag))) {
|
||||||
|
systemMessage = `${combinedSystemMessage}\n\n`;
|
||||||
|
} else {
|
||||||
|
conversationMessages.unshift(humanTag + combinedSystemMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prompt = `${systemMessage}${conversationMessages.join(splitter)}`;
|
||||||
|
|
||||||
|
// Ensure that the prompt ends with an assistant message
|
||||||
|
const lastHumanIndex = prompt.lastIndexOf(humanTag);
|
||||||
|
const lastAssistantIndex = prompt.lastIndexOf(assistantTag);
|
||||||
|
if (lastHumanIndex > lastAssistantIndex) {
|
||||||
|
prompt += splitter + assistantTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
// <System prompt/Character Card>
|
||||||
|
|
||||||
|
// ### Instruction:
|
||||||
|
// Your instruction or question here.
|
||||||
|
// For roleplay purposes, I suggest the following - Write <CHAR NAME>'s next reply in a chat between <YOUR NAME> and <CHAR NAME>. Write a single reply only.
|
||||||
|
|
||||||
|
// ### Response:
|
||||||
|
export const templateGryphePrompt = (messages: OpenpipeChatInput["messages"]) => {
|
||||||
|
const splitter = "\n\n";
|
||||||
|
|
||||||
|
const instructionTag = "### Instruction:\n";
|
||||||
|
const responseTag = "### Response:\n";
|
||||||
|
|
||||||
|
let combinedSystemMessage = "";
|
||||||
|
const conversationMessages = [];
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.role === "system") {
|
||||||
|
combinedSystemMessage += message.content;
|
||||||
|
} else if (message.role === "user") {
|
||||||
|
conversationMessages.push(instructionTag + message.content);
|
||||||
|
} else {
|
||||||
|
conversationMessages.push(responseTag + message.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let systemMessage = "";
|
||||||
|
|
||||||
|
if (combinedSystemMessage) {
|
||||||
|
// If there is no user message, add a user tag to the system message
|
||||||
|
if (conversationMessages.find((message) => message.startsWith(instructionTag))) {
|
||||||
|
systemMessage = `${combinedSystemMessage}\n\n`;
|
||||||
|
} else {
|
||||||
|
conversationMessages.unshift(instructionTag + combinedSystemMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prompt = `${systemMessage}${conversationMessages.join(splitter)}`;
|
||||||
|
|
||||||
|
// Ensure that the prompt ends with an assistant message
|
||||||
|
const lastInstructionIndex = prompt.lastIndexOf(instructionTag);
|
||||||
|
const lastAssistantIndex = prompt.lastIndexOf(responseTag);
|
||||||
|
if (lastInstructionIndex > lastAssistantIndex) {
|
||||||
|
prompt += splitter + responseTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt;
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ const replicate = new Replicate({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const modelIds: Record<ReplicateLlama2Input["model"], string> = {
|
const modelIds: Record<ReplicateLlama2Input["model"], string> = {
|
||||||
"7b-chat": "4f0b260b6a13eb53a6b1891f089d57c08f41003ae79458be5011303d81a394dc",
|
"7b-chat": "7b0bfc9aff140d5b75bacbed23e91fd3c34b01a1e958d32132de6e0a19796e2c",
|
||||||
"13b-chat": "2a7f981751ec7fdf87b5b91ad4db53683a98082e9ff7bfd12c8cd5ea85980a52",
|
"13b-chat": "2a7f981751ec7fdf87b5b91ad4db53683a98082e9ff7bfd12c8cd5ea85980a52",
|
||||||
"70b-chat": "2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1",
|
"70b-chat": "2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { type JSONSchema4 } from "json-schema";
|
|||||||
import { type IconType } from "react-icons";
|
import { type IconType } from "react-icons";
|
||||||
import { type JsonValue } from "type-fest";
|
import { type JsonValue } from "type-fest";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { type OpenpipeChatInput } from "./openpipe-chat";
|
||||||
|
|
||||||
export const ZodSupportedProvider = z.union([
|
export const ZodSupportedProvider = z.union([
|
||||||
z.literal("openai/ChatCompletion"),
|
z.literal("openai/ChatCompletion"),
|
||||||
z.literal("replicate/llama2"),
|
z.literal("replicate/llama2"),
|
||||||
z.literal("anthropic/completion"),
|
z.literal("anthropic/completion"),
|
||||||
|
z.literal("openpipe/Chat"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type SupportedProvider = z.infer<typeof ZodSupportedProvider>;
|
export type SupportedProvider = z.infer<typeof ZodSupportedProvider>;
|
||||||
@@ -22,6 +24,8 @@ export type Model = {
|
|||||||
description?: string;
|
description?: string;
|
||||||
learnMoreUrl?: string;
|
learnMoreUrl?: string;
|
||||||
apiDocsUrl?: string;
|
apiDocsUrl?: string;
|
||||||
|
templatePrompt?: (initialPrompt: OpenpipeChatInput["messages"]) => string;
|
||||||
|
defaultStopTokens?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProviderModel = { provider: z.infer<typeof ZodSupportedProvider>; model: string };
|
export type ProviderModel = { provider: z.infer<typeof ZodSupportedProvider>; model: string };
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { ChakraThemeProvider } from "~/theme/ChakraThemeProvider";
|
|||||||
import { SyncAppStore } from "~/state/sync";
|
import { SyncAppStore } from "~/state/sync";
|
||||||
import NextAdapterApp from "next-query-params/app";
|
import NextAdapterApp from "next-query-params/app";
|
||||||
import { QueryParamProvider } from "use-query-params";
|
import { QueryParamProvider } from "use-query-params";
|
||||||
import { SessionIdentifier } from "~/utils/analytics/clientAnalytics";
|
import { PosthogAppProvider } from "~/utils/analytics/posthog";
|
||||||
|
|
||||||
const MyApp: AppType<{ session: Session | null }> = ({
|
const MyApp: AppType<{ session: Session | null }> = ({
|
||||||
Component,
|
Component,
|
||||||
@@ -34,14 +34,15 @@ const MyApp: AppType<{ session: Session | null }> = ({
|
|||||||
<meta name="twitter:image" content="/og.png" />
|
<meta name="twitter:image" content="/og.png" />
|
||||||
</Head>
|
</Head>
|
||||||
<SessionProvider session={session}>
|
<SessionProvider session={session}>
|
||||||
<SyncAppStore />
|
<PosthogAppProvider>
|
||||||
<Favicon />
|
<SyncAppStore />
|
||||||
<SessionIdentifier />
|
<Favicon />
|
||||||
<ChakraThemeProvider>
|
<ChakraThemeProvider>
|
||||||
<QueryParamProvider adapter={NextAdapterApp}>
|
<QueryParamProvider adapter={NextAdapterApp}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</QueryParamProvider>
|
</QueryParamProvider>
|
||||||
</ChakraThemeProvider>
|
</ChakraThemeProvider>
|
||||||
|
</PosthogAppProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
54
app/src/pages/admin/jobs/index.tsx
Normal file
54
app/src/pages/admin/jobs/index.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Card, Table, Tbody, Td, Th, Thead, Tr } from "@chakra-ui/react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { isDate, isObject, isString } from "lodash-es";
|
||||||
|
import AppShell from "~/components/nav/AppShell";
|
||||||
|
import { type RouterOutputs, api } from "~/utils/api";
|
||||||
|
|
||||||
|
const fieldsToShow: (keyof RouterOutputs["adminJobs"]["list"][0])[] = [
|
||||||
|
"id",
|
||||||
|
"queue_name",
|
||||||
|
"payload",
|
||||||
|
"priority",
|
||||||
|
"attempts",
|
||||||
|
"last_error",
|
||||||
|
"created_at",
|
||||||
|
"key",
|
||||||
|
"locked_at",
|
||||||
|
"run_at",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Jobs() {
|
||||||
|
const jobs = api.adminJobs.list.useQuery({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell title="Admin Jobs">
|
||||||
|
<Card m={4} overflowX="auto">
|
||||||
|
<Table>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
{fieldsToShow.map((field) => (
|
||||||
|
<Th key={field}>{field}</Th>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{jobs.data?.map((job) => (
|
||||||
|
<Tr key={job.id}>
|
||||||
|
{fieldsToShow.map((field) => {
|
||||||
|
// Check if object
|
||||||
|
let value = job[field];
|
||||||
|
if (isDate(value)) {
|
||||||
|
value = dayjs(value).format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
} else if (isObject(value) && !isString(value)) {
|
||||||
|
value = JSON.stringify(value);
|
||||||
|
} // check if date
|
||||||
|
return <Td key={field}>{value}</Td>;
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// A faulty API route to test Sentry's error monitoring
|
|
||||||
// @ts-expect-error just a test file, don't care about types
|
|
||||||
export default function handler(_req, res) {
|
|
||||||
throw new Error("Sentry Example API Route Error");
|
|
||||||
res.status(200).json({ name: "John Doe" });
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
import cors from "nextjs-cors";
|
import cors from "nextjs-cors";
|
||||||
import { createOpenApiNextHandler } from "trpc-openapi";
|
import { createOpenApiNextHandler } from "trpc-openapi";
|
||||||
import { createProcedureCache } from "trpc-openapi/dist/adapters/node-http/procedures";
|
import { v1ApiRouter } from "~/server/api/external/v1Api.router";
|
||||||
import { appRouter } from "~/server/api/root.router";
|
import { createOpenApiContext } from "~/server/api/external/openApiTrpc";
|
||||||
import { createTRPCContext } from "~/server/api/trpc";
|
|
||||||
|
|
||||||
const openApiHandler = createOpenApiNextHandler({
|
const openApiHandler = createOpenApiNextHandler({
|
||||||
router: appRouter,
|
router: v1ApiRouter,
|
||||||
createContext: createTRPCContext,
|
createContext: createOpenApiContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cache = createProcedureCache(appRouter);
|
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
// Setup CORS
|
// Setup CORS
|
||||||
await cors(req, res);
|
await cors(req, res);
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
import { generateOpenApiDocument } from "trpc-openapi";
|
import { generateOpenApiDocument } from "trpc-openapi";
|
||||||
import { appRouter } from "~/server/api/root.router";
|
import { v1ApiRouter } from "~/server/api/external/v1Api.router";
|
||||||
|
|
||||||
export const openApiDocument = generateOpenApiDocument(appRouter, {
|
export const openApiDocument = generateOpenApiDocument(v1ApiRouter, {
|
||||||
title: "OpenPipe API",
|
title: "OpenPipe API",
|
||||||
description: "The public API for reporting API calls to OpenPipe",
|
description: "The public API for reporting API calls to OpenPipe",
|
||||||
version: "0.1.0",
|
version: "0.1.1",
|
||||||
baseUrl: "https://app.openpipe.ai/api",
|
baseUrl: "https://app.openpipe.ai/api/v1",
|
||||||
});
|
});
|
||||||
// Respond with our OpenAPI schema
|
// Respond with our OpenAPI schema
|
||||||
const hander = (req: NextApiRequest, res: NextApiResponse) => {
|
const hander = (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
@@ -15,20 +15,16 @@ import {
|
|||||||
Tr,
|
Tr,
|
||||||
Td,
|
Td,
|
||||||
Divider,
|
Divider,
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Ban, DollarSign, Hash } from "lucide-react";
|
import { Ban, DollarSign, Hash } from "lucide-react";
|
||||||
|
|
||||||
import AppShell from "~/components/nav/AppShell";
|
import AppShell from "~/components/nav/AppShell";
|
||||||
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
|
||||||
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
|
|
||||||
import { useSelectedProject } from "~/utils/hooks";
|
import { useSelectedProject } from "~/utils/hooks";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import LoggedCallTable from "~/components/dashboard/LoggedCallTable";
|
import LoggedCallsTable from "~/components/dashboard/LoggedCallsTable";
|
||||||
import UsageGraph from "~/components/dashboard/UsageGraph";
|
import UsageGraph from "~/components/dashboard/UsageGraph";
|
||||||
|
|
||||||
export default function LoggedCalls() {
|
export default function Dashboard() {
|
||||||
const { data: selectedProject } = useSelectedProject();
|
const { data: selectedProject } = useSelectedProject();
|
||||||
|
|
||||||
const stats = api.dashboard.stats.useQuery(
|
const stats = api.dashboard.stats.useQuery(
|
||||||
@@ -37,25 +33,15 @@ export default function LoggedCalls() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell title="Logged Calls" requireAuth>
|
<AppShell title="Dashboard" requireAuth requireBeta>
|
||||||
<PageHeaderContainer>
|
<VStack px={8} py={8} alignItems="flex-start" spacing={4}>
|
||||||
<Breadcrumb>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<ProjectBreadcrumbContents />
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbItem isCurrentPage>
|
|
||||||
<Text>Logged Calls</Text>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</Breadcrumb>
|
|
||||||
</PageHeaderContainer>
|
|
||||||
<VStack px={8} pt={4} alignItems="flex-start" spacing={4}>
|
|
||||||
<Text fontSize="2xl" fontWeight="bold">
|
<Text fontSize="2xl" fontWeight="bold">
|
||||||
{selectedProject?.name}
|
Dashboard
|
||||||
</Text>
|
</Text>
|
||||||
<Divider />
|
<Divider />
|
||||||
<VStack margin="auto" spacing={4} align="stretch" w="full">
|
<VStack margin="auto" spacing={4} align="stretch" w="full">
|
||||||
<HStack gap={4} align="start">
|
<HStack gap={4} align="start">
|
||||||
<Card variant="outline" flex={1}>
|
<Card flex={1}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<Heading as="h3" size="sm">
|
<Heading as="h3" size="sm">
|
||||||
Usage Statistics
|
Usage Statistics
|
||||||
@@ -66,7 +52,7 @@ export default function LoggedCalls() {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
<VStack spacing="4" width="300px" align="stretch">
|
<VStack spacing="4" width="300px" align="stretch">
|
||||||
<Card variant="outline">
|
<Card>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Stat>
|
<Stat>
|
||||||
<HStack>
|
<HStack>
|
||||||
@@ -79,7 +65,7 @@ export default function LoggedCalls() {
|
|||||||
</Stat>
|
</Stat>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
<Card variant="outline">
|
<Card>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Stat>
|
<Stat>
|
||||||
<HStack>
|
<HStack>
|
||||||
@@ -94,7 +80,7 @@ export default function LoggedCalls() {
|
|||||||
</Stat>
|
</Stat>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
<Card variant="outline" overflow="hidden">
|
<Card overflow="hidden">
|
||||||
<Stat>
|
<Stat>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<HStack>
|
<HStack>
|
||||||
@@ -120,7 +106,7 @@ export default function LoggedCalls() {
|
|||||||
</Card>
|
</Card>
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
<LoggedCallTable />
|
<LoggedCallsTable />
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
Center,
|
|
||||||
Flex,
|
|
||||||
Icon,
|
|
||||||
Input,
|
|
||||||
VStack,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { RiDatabase2Line } from "react-icons/ri";
|
|
||||||
import AppShell from "~/components/nav/AppShell";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useDataset, useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import DatasetEntriesTable from "~/components/datasets/DatasetEntriesTable";
|
|
||||||
import { DatasetHeaderButtons } from "~/components/datasets/DatasetHeaderButtons/DatasetHeaderButtons";
|
|
||||||
import PageHeaderContainer from "~/components/nav/PageHeaderContainer";
|
|
||||||
import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents";
|
|
||||||
|
|
||||||
export default function Dataset() {
|
|
||||||
const router = useRouter();
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const dataset = useDataset();
|
|
||||||
const datasetId = router.query.id as string;
|
|
||||||
|
|
||||||
const [name, setName] = useState(dataset.data?.name || "");
|
|
||||||
useEffect(() => {
|
|
||||||
setName(dataset.data?.name || "");
|
|
||||||
}, [dataset.data?.name]);
|
|
||||||
|
|
||||||
const updateMutation = api.datasets.update.useMutation();
|
|
||||||
const [onSaveName] = useHandledAsyncCallback(async () => {
|
|
||||||
if (name && name !== dataset.data?.name && dataset.data?.id) {
|
|
||||||
await updateMutation.mutateAsync({
|
|
||||||
id: dataset.data.id,
|
|
||||||
updates: { name: name },
|
|
||||||
});
|
|
||||||
await Promise.all([utils.datasets.list.invalidate(), utils.datasets.get.invalidate()]);
|
|
||||||
}
|
|
||||||
}, [updateMutation, dataset.data?.id, dataset.data?.name, name]);
|
|
||||||
|
|
||||||
if (!dataset.isLoading && !dataset.data) {
|
|
||||||
return (
|
|
||||||
<AppShell title="Dataset not found">
|
|
||||||
<Center h="100%">
|
|
||||||
<div>Dataset not found 😕</div>
|
|
||||||
</Center>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppShell title={dataset.data?.name}>
|
|
||||||
<VStack h="full">
|
|
||||||
<PageHeaderContainer>
|
|
||||||
<Breadcrumb>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<ProjectBreadcrumbContents projectName={dataset.data?.project?.name} />
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbItem>
|
|
||||||
<Link href="/data">
|
|
||||||
<Flex alignItems="center" _hover={{ textDecoration: "underline" }}>
|
|
||||||
<Icon as={RiDatabase2Line} boxSize={4} mr={2} /> Datasets
|
|
||||||
</Flex>
|
|
||||||
</Link>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbItem isCurrentPage>
|
|
||||||
<Input
|
|
||||||
size="sm"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
onBlur={onSaveName}
|
|
||||||
borderWidth={1}
|
|
||||||
borderColor="transparent"
|
|
||||||
fontSize={16}
|
|
||||||
px={0}
|
|
||||||
minW={{ base: 100, lg: 300 }}
|
|
||||||
flex={1}
|
|
||||||
_hover={{ borderColor: "gray.300" }}
|
|
||||||
_focus={{ borderColor: "blue.500", outline: "none" }}
|
|
||||||
/>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</Breadcrumb>
|
|
||||||
<DatasetHeaderButtons />
|
|
||||||
</PageHeaderContainer>
|
|
||||||
<Box w="full" overflowX="auto" flex={1} px={8} pt={8} pb={16}>
|
|
||||||
{datasetId && <DatasetEntriesTable />}
|
|
||||||
</Box>
|
|
||||||
</VStack>
|
|
||||||
</AppShell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user