Compare commits
1 Commits
upload-jso
...
styling
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e56c93c3f |
@@ -1,5 +0,0 @@
|
|||||||
**/node_modules/
|
|
||||||
.git
|
|
||||||
**/.venv/
|
|
||||||
**/.env*
|
|
||||||
**/.next/
|
|
||||||
14
.github/ISSUE_TEMPLATE/sweep-fast-template.yml
vendored
14
.github/ISSUE_TEMPLATE/sweep-fast-template.yml
vendored
@@ -1,14 +0,0 @@
|
|||||||
name: Sweep Fast Issue
|
|
||||||
title: 'Sweep (fast): '
|
|
||||||
description: For few-line fixes to be handled by Sweep, an AI-powered junior developer. Sweep will use GPT-3.5 to quickly create a PR for very small changes.
|
|
||||||
labels: sweep
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Details
|
|
||||||
description: Tell Sweep where and what to edit and provide enough context for a new developer to the codebase
|
|
||||||
placeholder: |
|
|
||||||
Bugs: The bug might be in ... file. Here are the logs: ...
|
|
||||||
Features: the new endpoint should use the ... class from ... file because it contains ... logic.
|
|
||||||
Refactors: We are migrating this function to ... version because ...
|
|
||||||
14
.github/ISSUE_TEMPLATE/sweep-slow-template.yml
vendored
14
.github/ISSUE_TEMPLATE/sweep-slow-template.yml
vendored
@@ -1,14 +0,0 @@
|
|||||||
name: Sweep Slow Issue
|
|
||||||
title: 'Sweep (slow): '
|
|
||||||
description: For larger bugs, features, refactors, and tests to be handled by Sweep, an AI-powered junior developer. Sweep will perform a deeper search and more self-reviews but will take longer.
|
|
||||||
labels: sweep
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Details
|
|
||||||
description: Tell Sweep where and what to edit and provide enough context for a new developer to the codebase
|
|
||||||
placeholder: |
|
|
||||||
Bugs: The bug might be in ... file. Here are the logs: ...
|
|
||||||
Features: the new endpoint should use the ... class from ... file because it contains ... logic.
|
|
||||||
Refactors: We are migrating this function to ... version because ...
|
|
||||||
14
.github/ISSUE_TEMPLATE/sweep-template.yml
vendored
14
.github/ISSUE_TEMPLATE/sweep-template.yml
vendored
@@ -1,14 +0,0 @@
|
|||||||
name: Sweep Issue
|
|
||||||
title: 'Sweep: '
|
|
||||||
description: For small bugs, features, refactors, and tests to be handled by Sweep, an AI-powered junior developer.
|
|
||||||
labels: sweep
|
|
||||||
body:
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Details
|
|
||||||
description: Tell Sweep where and what to edit and provide enough context for a new developer to the codebase
|
|
||||||
placeholder: |
|
|
||||||
Bugs: The bug might be in ... file. Here are the logs: ...
|
|
||||||
Features: the new endpoint should use the ... class from ... file because it contains ... logic.
|
|
||||||
Refactors: We are migrating this function to ... version because ...
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,4 +3,3 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
node_modules/
|
node_modules/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
dist/
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
*.schema.json
|
|
||||||
app/pnpm-lock.yaml
|
|
||||||
105
README.md
105
README.md
@@ -1,53 +1,16 @@
|
|||||||
<p align="center">
|
<!-- <img src="https://github.com/openpipe/openpipe/assets/41524992/ca59596e-eb80-40f9-921f-6d67f6e6d8fa" width="72px" /> -->
|
||||||
<a href="https://openpipe.ai">
|
|
||||||
<img height="70" src="https://github.com/openpipe/openpipe/assets/41524992/70af25fb-1f90-42d9-8a20-3606e3b5aaba" alt="logo">
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<h1 align="center">
|
|
||||||
OpenPipe
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p align="center">
|
# OpenPipe
|
||||||
<i>Turn expensive prompts into cheap fine-tuned models.</i>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
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.
|
||||||
<a href="/LICENSE"><img alt="License Apache-2.0" src="https://img.shields.io/github/license/openpipe/openpipe?style=flat-square"></a>
|
|
||||||
<a href='http://makeapullrequest.com'><img alt='PRs Welcome' src='https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square'/></a>
|
|
||||||
<a href="https://github.com/openpipe/openpipe/graphs/commit-activity"><img alt="GitHub commit activity" src="https://img.shields.io/github/commit-activity/m/openpipe/openpipe?style=flat-square"/></a>
|
|
||||||
<a href="https://github.com/openpipe/openpipe/issues"><img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/openpipe/openpipe?style=flat-square"/></a>
|
|
||||||
<img src="https://img.shields.io/badge/Y%20Combinator-S23-orange?style=flat-square" alt="Y Combinator S23">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
<img src="https://github.com/openpipe/openpipe/assets/41524992/219a844e-3f4e-4f6b-8066-41348b42977b" alt="demo">
|
||||||
<a href="https://app.openpipe.ai/">Hosted App</a> - <a href="#running-locally">Running Locally</a> - <a href="#sample-experiments">Experiments</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<br>
|
|
||||||
Use powerful but expensive LLMs to fine-tune smaller and cheaper models suited to your exact needs. Evaluate model and prompt combinations in the playground. Query your past requests and export optimized training data. Try it out at https://app.openpipe.ai or <a href="#running-locally">run it locally</a>.
|
|
||||||
<br>
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* <b>Experiment</b>
|
|
||||||
* Bulk-test wide-reaching scenarios using code templating.
|
|
||||||
* Seamlessly translate prompts across different model APIs.
|
|
||||||
* Tap into autogenerated scenarios for fresh test perspectives.
|
|
||||||
|
|
||||||
* <b>Fine-Tune (Beta)</b>
|
|
||||||
* Easy integration with OpenPipe's SDK in both Python and JS.
|
|
||||||
* Swiftly query logs using intuitive built-in filters.
|
|
||||||
* Export data in multiple training formats, including Alpaca and ChatGPT, with deduplication.
|
|
||||||
|
|
||||||
<img src="https://github.com/openpipe/openpipe/assets/41524992/eaa8b92d-4536-4f63-bbef-4b0b1a60f6b5" alt="fine-tune demo">
|
|
||||||
|
|
||||||
<!-- <img height="400px" src="https://github.com/openpipe/openpipe/assets/41524992/66bb1843-cb72-4130-a369-eec2df3b8201" alt="playground 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).
|
||||||
|
|
||||||
## Sample Experiments
|
## Sample Experiments
|
||||||
|
|
||||||
These are sample experiments users have created that show how OpenPipe works. Feel free to fork them and start experimenting yourself.
|
These are simple experiments users have created that show how OpenPipe works. Feel free to fork them and start experimenting yourself.
|
||||||
|
|
||||||
- [Twitter Sentiment Analysis](https://app.openpipe.ai/experiments/62c20a73-2012-4a64-973c-4b665ad46a57)
|
- [Twitter Sentiment Analysis](https://app.openpipe.ai/experiments/62c20a73-2012-4a64-973c-4b665ad46a57)
|
||||||
- [Reddit User Needs](https://app.openpipe.ai/experiments/22222222-2222-2222-2222-222222222222)
|
- [Reddit User Needs](https://app.openpipe.ai/experiments/22222222-2222-2222-2222-222222222222)
|
||||||
@@ -56,25 +19,43 @@ These are sample experiments users have created that show how OpenPipe works. Fe
|
|||||||
|
|
||||||
## Supported Models
|
## Supported Models
|
||||||
|
|
||||||
#### OpenAI
|
- All models available through the OpenAI [chat completion API](https://platform.openai.com/docs/guides/gpt/chat-completions-api)
|
||||||
- [GPT 3.5 Turbo](https://platform.openai.com/docs/guides/gpt/chat-completions-api)
|
- Llama2 [7b chat](https://replicate.com/a16z-infra/llama7b-v2-chat), [13b chat](https://replicate.com/a16z-infra/llama13b-v2-chat), [70b chat](https://replicate.com/replicate/llama70b-v2-chat).
|
||||||
- [GPT 3.5 Turbo 16k](https://platform.openai.com/docs/guides/gpt/chat-completions-api)
|
- Anthropic's [Claude 1 Instant](https://www.anthropic.com/index/introducing-claude) and [Claude 2](https://www.anthropic.com/index/claude-2)
|
||||||
- [GPT 4](https://openai.com/gpt-4)
|
|
||||||
#### Llama2
|
## Features
|
||||||
- [7b chat](https://replicate.com/a16z-infra/llama7b-v2-chat)
|
|
||||||
- [13b chat](https://replicate.com/a16z-infra/llama13b-v2-chat)
|
### 🔍 Visualize Responses
|
||||||
- [70b chat](https://replicate.com/replicate/llama70b-v2-chat)
|
|
||||||
#### Llama2 Fine-Tunes
|
Inspect prompt completions side-by-side.
|
||||||
- [Open-Orca/OpenOrcaxOpenChat-Preview2-13B](https://huggingface.co/Open-Orca/OpenOrcaxOpenChat-Preview2-13B)
|
|
||||||
- [Open-Orca/OpenOrca-Platypus2-13B](https://huggingface.co/Open-Orca/OpenOrca-Platypus2-13B)
|
### 🧪 Bulk-Test
|
||||||
- [NousResearch/Nous-Hermes-Llama2-13b](https://huggingface.co/NousResearch/Nous-Hermes-Llama2-13b)
|
|
||||||
- [jondurbin/airoboros-l2-13b-gpt4-2.0](https://huggingface.co/jondurbin/airoboros-l2-13b-gpt4-2.0)
|
OpenPipe lets you _template_ a prompt. Use the templating feature to run the prompts you're testing against many potential inputs for broad coverage of your problem space.
|
||||||
- [lmsys/vicuna-13b-v1.5](https://huggingface.co/lmsys/vicuna-13b-v1.5)
|
|
||||||
- [Gryphe/MythoMax-L2-13b](https://huggingface.co/Gryphe/MythoMax-L2-13b)
|
### 📟 Translate between Model APIs
|
||||||
- [NousResearch/Nous-Hermes-llama-2-7b](https://huggingface.co/NousResearch/Nous-Hermes-llama-2-7b)
|
|
||||||
#### Anthropic
|
Write your prompt in one format and automatically convert it to work with any other model.
|
||||||
- [Claude 1 Instant](https://www.anthropic.com/index/introducing-claude)
|
|
||||||
- [Claude 2](https://www.anthropic.com/index/claude-2)
|
<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
|
||||||
|
|
||||||
|
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">
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
### 🪄 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!
|
||||||
|
|
||||||
|
<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,16 +32,5 @@ 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/v1"
|
OPENPIPE_BASE_URL="http://localhost:3000/api"
|
||||||
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"
|
|
||||||
|
|
||||||
# Azure credentials are necessary for uploading large training data files
|
|
||||||
AZURE_STORAGE_ACCOUNT_NAME="placeholder"
|
|
||||||
AZURE_STORAGE_ACCOUNT_KEY="placeholder"
|
|
||||||
AZURE_STORAGE_CONTAINER_NAME="placeholder"
|
|
||||||
|
|||||||
2
app/.prettierignore
Normal file
2
app/.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*.schema.json
|
||||||
|
pnpm-lock.yaml
|
||||||
14
app/@types/nextjs-routes.d.ts
vendored
14
app/@types/nextjs-routes.d.ts
vendored
@@ -12,20 +12,18 @@ declare module "nextjs-routes" {
|
|||||||
|
|
||||||
export type Route =
|
export type Route =
|
||||||
| StaticRoute<"/account/signin">
|
| StaticRoute<"/account/signin">
|
||||||
| StaticRoute<"/admin/jobs">
|
| DynamicRoute<"/api/[...trpc]", { "trpc": string[] }>
|
||||||
| 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<"/api/v1/[...trpc]", { "trpc": string[] }>
|
|
||||||
| StaticRoute<"/api/v1/openapi">
|
|
||||||
| StaticRoute<"/dashboard">
|
| StaticRoute<"/dashboard">
|
||||||
| DynamicRoute<"/datasets/[id]", { "id": string }>
|
| DynamicRoute<"/data/[id]", { "id": string }>
|
||||||
| StaticRoute<"/datasets">
|
| StaticRoute<"/data">
|
||||||
| DynamicRoute<"/experiments/[experimentSlug]", { "experimentSlug": string }>
|
| DynamicRoute<"/experiments/[id]", { "id": string }>
|
||||||
| StaticRoute<"/experiments">
|
| StaticRoute<"/experiments">
|
||||||
| StaticRoute<"/fine-tunes">
|
|
||||||
| StaticRoute<"/">
|
| StaticRoute<"/">
|
||||||
| DynamicRoute<"/invitations/[invitationToken]", { "invitationToken": string }>
|
|
||||||
| StaticRoute<"/project/settings">
|
| StaticRoute<"/project/settings">
|
||||||
| StaticRoute<"/request-logs">
|
| StaticRoute<"/request-logs">
|
||||||
| StaticRoute<"/sentry-example-page">
|
| StaticRoute<"/sentry-example-page">
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ RUN yarn global add pnpm
|
|||||||
# DEPS
|
# DEPS
|
||||||
FROM base as deps
|
FROM base as deps
|
||||||
|
|
||||||
WORKDIR /code
|
WORKDIR /app
|
||||||
|
|
||||||
COPY app/prisma app/package.json ./app/
|
COPY prisma ./
|
||||||
COPY client-libs/typescript/package.json ./client-libs/typescript/
|
|
||||||
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
||||||
|
|
||||||
RUN cd app && pnpm install --frozen-lockfile
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# BUILDER
|
# BUILDER
|
||||||
FROM base as builder
|
FROM base as builder
|
||||||
@@ -23,25 +23,24 @@ 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 /code
|
WORKDIR /app
|
||||||
COPY --from=deps /code/node_modules ./node_modules
|
COPY --from=deps /app/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 cd app && SKIP_ENV_VALIDATION=1 pnpm build
|
RUN SKIP_ENV_VALIDATION=1 pnpm build
|
||||||
|
|
||||||
# RUNNER
|
# RUNNER
|
||||||
FROM base as runner
|
FROM base as runner
|
||||||
WORKDIR /code/app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
COPY --from=builder /code/ /code/
|
COPY --from=builder /app/ ./
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
ENV PORT 3000
|
ENV PORT 3000
|
||||||
|
|
||||||
# Run the "run-prod.sh" script
|
# Run the "run-prod.sh" script
|
||||||
CMD /code/app/scripts/run-prod.sh
|
CMD /app/run-prod.sh
|
||||||
@@ -10,15 +10,14 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev:next": "TZ=UTC next dev",
|
"dev:next": "next dev",
|
||||||
"dev:wss": "pnpm tsx --watch src/wss-server.ts",
|
"dev:wss": "pnpm tsx --watch src/wss-server.ts",
|
||||||
"worker": "NODE_ENV='development' pnpm tsx --watch src/server/tasks/worker.ts",
|
"dev:worker": "NODE_ENV='development' pnpm tsx --watch src/server/tasks/worker.ts",
|
||||||
"dev": "concurrently --kill-others 'pnpm dev:next' 'pnpm dev:wss' 'pnpm worker --watch'",
|
"dev": "concurrently --kill-others 'pnpm dev:next' 'pnpm dev:wss' 'pnpm dev:worker'",
|
||||||
"postinstall": "prisma generate",
|
"postinstall": "prisma generate",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start": "TZ=UTC next start",
|
"start": "next start",
|
||||||
"codegen:clients": "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"
|
||||||
@@ -26,8 +25,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",
|
||||||
"@azure/identity": "^3.3.0",
|
|
||||||
"@azure/storage-blob": "12.15.0",
|
|
||||||
"@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",
|
||||||
@@ -40,7 +37,6 @@
|
|||||||
"@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",
|
||||||
@@ -50,7 +46,6 @@
|
|||||||
"@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",
|
||||||
@@ -63,27 +58,20 @@
|
|||||||
"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",
|
|
||||||
"llama-tokenizer-js": "^1.1.3",
|
|
||||||
"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": "^0.3.0",
|
|
||||||
"openpipe-dev": "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",
|
||||||
@@ -104,7 +92,6 @@
|
|||||||
"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",
|
||||||
@@ -113,11 +100,11 @@
|
|||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite-tsconfig-paths": "^4.2.0",
|
"vite-tsconfig-paths": "^4.2.0",
|
||||||
"zod": "^3.21.4",
|
"zod": "^3.21.4",
|
||||||
"zustand": "^4.3.9"
|
"zustand": "^4.3.9",
|
||||||
|
"openpipe": "workspace:*"
|
||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -127,14 +114,12 @@
|
|||||||
"@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",
|
||||||
@@ -144,7 +129,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { prisma } from "~/server/db";
|
|
||||||
|
|
||||||
// delete most recent fineTune
|
|
||||||
const mostRecentFineTune = await prisma.fineTune.findFirst({
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (mostRecentFineTune) {
|
|
||||||
await prisma.fineTune.delete({
|
|
||||||
where: { id: mostRecentFineTune.id },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
-- 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");
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
-- 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;
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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");
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
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;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- Added the required column `inputTokens` to the `DatasetEntry` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `outputTokens` to the `DatasetEntry` table without a default value. This is not possible if the table is not empty.
|
|
||||||
- Added the required column `type` to the `DatasetEntry` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- CreateEnum
|
|
||||||
CREATE TYPE "DatasetEntryType" AS ENUM ('TRAIN', 'TEST');
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "Dataset" ADD COLUMN "trainingRatio" DOUBLE PRECISION NOT NULL DEFAULT 0.8;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "DatasetEntry" ADD COLUMN "input" JSONB NOT NULL DEFAULT '[]',
|
|
||||||
ADD COLUMN "inputTokens" INTEGER NOT NULL DEFAULT 0,
|
|
||||||
ADD COLUMN "output" JSONB,
|
|
||||||
ADD COLUMN "outputTokens" INTEGER NOT NULL DEFAULT 0,
|
|
||||||
ADD COLUMN "type" "DatasetEntryType" NOT NULL DEFAULT 'TRAIN';
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "DatasetEntry_datasetId_createdAt_id_idx" ON "DatasetEntry"("datasetId", "createdAt", "id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "DatasetEntry_datasetId_type_idx" ON "DatasetEntry"("datasetId", "type");
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "DatasetEntry" ALTER COLUMN "loggedCallId" DROP NOT NULL,
|
|
||||||
ALTER COLUMN "inputTokens" DROP DEFAULT,
|
|
||||||
ALTER COLUMN "outputTokens" DROP DEFAULT,
|
|
||||||
ALTER COLUMN "type" DROP DEFAULT;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
-- CreateEnum
|
|
||||||
CREATE TYPE "DatasetFileUploadStatus" AS ENUM ('PENDING', 'DOWNLOADING', 'PROCESSING', 'SAVING', 'COMPLETE', 'ERROR');
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "DatasetFileUpload" (
|
|
||||||
"id" UUID NOT NULL,
|
|
||||||
"datasetId" UUID NOT NULL,
|
|
||||||
"blobName" TEXT NOT NULL,
|
|
||||||
"fileName" TEXT NOT NULL,
|
|
||||||
"fileSize" INTEGER NOT NULL,
|
|
||||||
"progress" INTEGER NOT NULL DEFAULT 0,
|
|
||||||
"status" "DatasetFileUploadStatus" NOT NULL DEFAULT 'PENDING',
|
|
||||||
"uploadedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
"visible" BOOLEAN NOT NULL DEFAULT true,
|
|
||||||
"errorMessage" TEXT,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "DatasetFileUpload_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "DatasetFileUpload" ADD CONSTRAINT "DatasetFileUpload_datasetId_fkey" FOREIGN KEY ("datasetId") REFERENCES "Dataset"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -12,8 +12,6 @@ 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)
|
||||||
@@ -176,42 +174,11 @@ model OutputEvaluation {
|
|||||||
@@unique([modelResponseId, evaluationId])
|
@@unique([modelResponseId, evaluationId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum DatasetFileUploadStatus {
|
|
||||||
PENDING
|
|
||||||
DOWNLOADING
|
|
||||||
PROCESSING
|
|
||||||
SAVING
|
|
||||||
COMPLETE
|
|
||||||
ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
model DatasetFileUpload {
|
|
||||||
id String @id @default(uuid()) @db.Uuid
|
|
||||||
|
|
||||||
datasetId String @db.Uuid
|
|
||||||
dataset Dataset @relation(fields: [datasetId], references: [id], onDelete: Cascade)
|
|
||||||
blobName String
|
|
||||||
fileName String
|
|
||||||
fileSize Int
|
|
||||||
progress Int @default(0) // Percentage
|
|
||||||
status DatasetFileUploadStatus @default(PENDING)
|
|
||||||
uploadedAt DateTime
|
|
||||||
visible Boolean @default(true)
|
|
||||||
errorMessage String?
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
}
|
|
||||||
|
|
||||||
model Dataset {
|
model Dataset {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
name String
|
name String
|
||||||
datasetEntries DatasetEntry[]
|
datasetEntries DatasetEntry[]
|
||||||
fineTunes FineTune[]
|
|
||||||
datasetFileUploads DatasetFileUpload[]
|
|
||||||
trainingRatio Float @default(0.8)
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -220,32 +187,17 @@ model Dataset {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DatasetEntryType {
|
|
||||||
TRAIN
|
|
||||||
TEST
|
|
||||||
}
|
|
||||||
|
|
||||||
model DatasetEntry {
|
model DatasetEntry {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
||||||
loggedCallId String? @db.Uuid
|
input String
|
||||||
loggedCall LoggedCall? @relation(fields: [loggedCallId], references: [id], onDelete: Cascade)
|
output String?
|
||||||
|
|
||||||
input Json @default("[]")
|
|
||||||
output Json?
|
|
||||||
inputTokens Int
|
|
||||||
outputTokens Int
|
|
||||||
|
|
||||||
type DatasetEntryType
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@index([datasetId, createdAt, id])
|
|
||||||
@@index([datasetId, type])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Project {
|
model Project {
|
||||||
@@ -258,11 +210,9 @@ model Project {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
projectUsers ProjectUser[]
|
projectUsers ProjectUser[]
|
||||||
projectUserInvitations UserInvitation[]
|
|
||||||
experiments Experiment[]
|
experiments Experiment[]
|
||||||
datasets Dataset[]
|
datasets Dataset[]
|
||||||
loggedCalls LoggedCall[]
|
loggedCalls LoggedCall[]
|
||||||
fineTunes FineTune[]
|
|
||||||
apiKeys ApiKey[]
|
apiKeys ApiKey[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +275,6 @@ model LoggedCall {
|
|||||||
|
|
||||||
model String?
|
model String?
|
||||||
tags LoggedCallTag[]
|
tags LoggedCallTag[]
|
||||||
datasetEntries DatasetEntry[]
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
@@ -360,7 +309,7 @@ model LoggedCallModelResponse {
|
|||||||
outputTokens Int?
|
outputTokens Int?
|
||||||
finishReason String?
|
finishReason String?
|
||||||
completionId String?
|
completionId String?
|
||||||
cost Float?
|
cost Decimal? @db.Decimal(18, 12)
|
||||||
|
|
||||||
// The LoggedCall that created this LoggedCallModelResponse
|
// The LoggedCall that created this LoggedCallModelResponse
|
||||||
originalLoggedCallId String @unique @db.Uuid
|
originalLoggedCallId String @unique @db.Uuid
|
||||||
@@ -377,14 +326,12 @@ 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)
|
||||||
|
|
||||||
@@unique([loggedCallId, name])
|
@@index([name])
|
||||||
@@index([projectId, name])
|
@@index([name, value])
|
||||||
@@index([projectId, name, value])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model ApiKey {
|
model ApiKey {
|
||||||
@@ -394,7 +341,7 @@ model ApiKey {
|
|||||||
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
|
||||||
@@ -446,28 +393,11 @@ model User {
|
|||||||
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
|
||||||
@@ -475,33 +405,3 @@ 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,7 +2,6 @@ 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";
|
||||||
|
|
||||||
@@ -10,14 +9,6 @@ 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({})) ??
|
||||||
@@ -25,20 +16,6 @@ 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,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
|
import { generateNewCell } from "~/server/utils/generateNewCell";
|
||||||
import dedent from "dedent";
|
import dedent from "dedent";
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: number;
|
inputTokens: number;
|
||||||
outputTokens: number;
|
outputTokens: number;
|
||||||
finishReason: string;
|
finishReason: string;
|
||||||
tags: { name: string; value: string }[];
|
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -80,7 +79,7 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
},
|
},
|
||||||
respStatus: 200,
|
respStatus: 200,
|
||||||
respPayload: {
|
respPayload: {
|
||||||
id: "chatcmpl-7",
|
id: "chatcmpl-7lNspqePJWVyXwXebupxb1eMozo6Q",
|
||||||
model: "gpt-3.5-turbo-0613",
|
model: "gpt-3.5-turbo-0613",
|
||||||
usage: {
|
usage: {
|
||||||
total_tokens: 241,
|
total_tokens: 241,
|
||||||
@@ -108,7 +107,6 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 236,
|
inputTokens: 236,
|
||||||
outputTokens: 5,
|
outputTokens: 5,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
tags: [{ name: "prompt_id", value: "add_scenario" }],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -167,7 +165,7 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
},
|
},
|
||||||
respStatus: 200,
|
respStatus: 200,
|
||||||
respPayload: {
|
respPayload: {
|
||||||
id: "chatcmpl-7",
|
id: "chatcmpl-7lNifmc5AncyAvleZRDBhAcLFYBIT",
|
||||||
model: "gpt-3.5-turbo-0613",
|
model: "gpt-3.5-turbo-0613",
|
||||||
usage: {
|
usage: {
|
||||||
total_tokens: 227,
|
total_tokens: 227,
|
||||||
@@ -195,7 +193,6 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 222,
|
inputTokens: 222,
|
||||||
outputTokens: 5,
|
outputTokens: 5,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
tags: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -210,7 +207,7 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
},
|
},
|
||||||
respStatus: 200,
|
respStatus: 200,
|
||||||
respPayload: {
|
respPayload: {
|
||||||
id: "chatcmpl-7",
|
id: "chatcmpl-7lNh1TtrsJVgz3Nj70bKkZZk7xPi7",
|
||||||
model: "gpt-3.5-turbo-0613",
|
model: "gpt-3.5-turbo-0613",
|
||||||
usage: {
|
usage: {
|
||||||
total_tokens: 21,
|
total_tokens: 21,
|
||||||
@@ -234,7 +231,6 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 14,
|
inputTokens: 14,
|
||||||
outputTokens: 7,
|
outputTokens: 7,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
tags: [{ name: "prompt_id", value: "translate_text" }],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reqPayload: {
|
reqPayload: {
|
||||||
@@ -281,7 +277,7 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
},
|
},
|
||||||
respStatus: 200,
|
respStatus: 200,
|
||||||
respPayload: {
|
respPayload: {
|
||||||
id: "chatcmpl-7",
|
id: "chatcmpl-7lQS3MktOT8BTgNEytl9dkyssCQqL",
|
||||||
model: "gpt-4-0613",
|
model: "gpt-4-0613",
|
||||||
usage: {
|
usage: {
|
||||||
total_tokens: 2910,
|
total_tokens: 2910,
|
||||||
@@ -310,10 +306,6 @@ const MODEL_RESPONSE_TEMPLATES: {
|
|||||||
inputTokens: 2802,
|
inputTokens: 2802,
|
||||||
outputTokens: 108,
|
outputTokens: 108,
|
||||||
finishReason: "stop",
|
finishReason: "stop",
|
||||||
tags: [
|
|
||||||
{ name: "prompt_id", value: "define_func" },
|
|
||||||
{ name: "some_other_tag", value: "some_other_value" },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -339,7 +331,7 @@ const loggedCallsToCreate: Prisma.LoggedCallCreateManyInput[] = [];
|
|||||||
const loggedCallModelResponsesToCreate: Prisma.LoggedCallModelResponseCreateManyInput[] = [];
|
const loggedCallModelResponsesToCreate: Prisma.LoggedCallModelResponseCreateManyInput[] = [];
|
||||||
const loggedCallsToUpdate: Prisma.LoggedCallUpdateArgs[] = [];
|
const loggedCallsToUpdate: Prisma.LoggedCallUpdateArgs[] = [];
|
||||||
const loggedCallTagsToCreate: Prisma.LoggedCallTagCreateManyInput[] = [];
|
const loggedCallTagsToCreate: Prisma.LoggedCallTagCreateManyInput[] = [];
|
||||||
for (let i = 0; i < 11437; i++) {
|
for (let i = 0; i < 1437; i++) {
|
||||||
const loggedCallId = uuidv4();
|
const loggedCallId = uuidv4();
|
||||||
const loggedCallModelResponseId = uuidv4();
|
const loggedCallModelResponseId = uuidv4();
|
||||||
const template =
|
const template =
|
||||||
@@ -357,7 +349,6 @@ for (let i = 0; i < 11437; i++) {
|
|||||||
cacheHit: false,
|
cacheHit: false,
|
||||||
requestedAt,
|
requestedAt,
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
model: template.reqPayload.model,
|
|
||||||
createdAt: requestedAt,
|
createdAt: requestedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -397,15 +388,12 @@ for (let i = 0; i < 11437; i++) {
|
|||||||
modelResponseId: loggedCallModelResponseId,
|
modelResponseId: loggedCallModelResponseId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
for (const tag of template.tags) {
|
|
||||||
loggedCallTagsToCreate.push({
|
loggedCallTagsToCreate.push({
|
||||||
projectId: project.id,
|
|
||||||
loggedCallId,
|
loggedCallId,
|
||||||
name: tag.name,
|
name: "$model",
|
||||||
value: tag.value,
|
value: template.reqPayload.model,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$transaction([
|
await prisma.$transaction([
|
||||||
prisma.loggedCall.createMany({
|
prisma.loggedCall.createMany({
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ pnpm tsx src/promptConstructor/migrate.ts
|
|||||||
|
|
||||||
echo "Starting the server"
|
echo "Starting the server"
|
||||||
|
|
||||||
pnpm start
|
pnpm concurrently --kill-others \
|
||||||
|
"pnpm start" \
|
||||||
|
"pnpm tsx src/server/tasks/worker.ts"
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#! /bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y htop psql
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#! /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"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#! /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,7 +3,6 @@
|
|||||||
// 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) {
|
||||||
@@ -16,10 +15,4 @@ 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,55 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { Button, HStack, type ButtonProps, Icon, Text } from "@chakra-ui/react";
|
|
||||||
import { type IconType } from "react-icons";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import { BetaModal } from "./BetaModal";
|
|
||||||
|
|
||||||
const ActionButton = ({
|
|
||||||
icon,
|
|
||||||
iconBoxSize = 3.5,
|
|
||||||
label,
|
|
||||||
requireBeta = false,
|
|
||||||
onClick,
|
|
||||||
...buttonProps
|
|
||||||
}: {
|
|
||||||
icon: IconType;
|
|
||||||
iconBoxSize?: number;
|
|
||||||
label: string;
|
|
||||||
requireBeta?: boolean;
|
|
||||||
onClick?: () => void;
|
|
||||||
} & ButtonProps) => {
|
|
||||||
const flags = useAppStore((s) => s.featureFlags.featureFlags);
|
|
||||||
const flagsLoaded = useAppStore((s) => s.featureFlags.flagsLoaded);
|
|
||||||
|
|
||||||
const [betaModalOpen, setBetaModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const isBetaBlocked = requireBeta && flagsLoaded && !flags.betaAccess;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
color="black"
|
|
||||||
bgColor="white"
|
|
||||||
borderColor="gray.300"
|
|
||||||
borderRadius={4}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
fontSize="sm"
|
|
||||||
fontWeight="normal"
|
|
||||||
onClick={isBetaBlocked ? () => setBetaModalOpen(true) : onClick}
|
|
||||||
{...buttonProps}
|
|
||||||
>
|
|
||||||
<HStack spacing={1}>
|
|
||||||
{icon && (
|
|
||||||
<Icon as={icon} boxSize={iconBoxSize} color={requireBeta ? "orange.400" : undefined} />
|
|
||||||
)}
|
|
||||||
<Text display={{ base: "none", md: "flex" }}>{label}</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
<BetaModal isOpen={betaModalOpen} onClose={() => setBetaModalOpen(false)} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ActionButton;
|
|
||||||
@@ -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, { useEffect, useState } from "react";
|
import React, { useLayoutEffect, 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);
|
||||||
useEffect(() => setIsRerendered(true), []);
|
useLayoutEffect(() => setIsRerendered(true), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import {
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
VStack,
|
|
||||||
Text,
|
|
||||||
HStack,
|
|
||||||
Icon,
|
|
||||||
Link,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { BsStars } from "react-icons/bs";
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
|
|
||||||
export const BetaModal = ({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) => {
|
|
||||||
const session = useSession();
|
|
||||||
|
|
||||||
const email = session.data?.user.email ?? "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
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={onClose}>
|
|
||||||
Done
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useState, useMemo, useCallback } from "react";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
HStack,
|
HStack,
|
||||||
@@ -15,18 +14,16 @@ import {
|
|||||||
VStack,
|
VStack,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { type PromptVariant } from "@prisma/client";
|
import { type PromptVariant } from "@prisma/client";
|
||||||
import { isString } from "lodash-es";
|
import { isObject, isString } from "lodash-es";
|
||||||
|
import { useState } from "react";
|
||||||
import { RiExchangeFundsFill } from "react-icons/ri";
|
import { RiExchangeFundsFill } from "react-icons/ri";
|
||||||
|
|
||||||
import { type ProviderModel } from "~/modelProviders/types";
|
import { type ProviderModel } from "~/modelProviders/types";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
||||||
import { lookupModel, modelLabel } from "~/utils/utils";
|
import { lookupModel, modelLabel } from "~/utils/utils";
|
||||||
import CompareFunctions from "../RefinePromptModal/CompareFunctions";
|
import CompareFunctions from "../RefinePromptModal/CompareFunctions";
|
||||||
import { ModelSearch } from "./ModelSearch";
|
import { ModelSearch } from "./ModelSearch";
|
||||||
import { ModelStatsCard } from "./ModelStatsCard";
|
import { ModelStatsCard } from "./ModelStatsCard";
|
||||||
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
|
|
||||||
export const ChangeModelModal = ({
|
export const ChangeModelModal = ({
|
||||||
variant,
|
variant,
|
||||||
@@ -35,43 +32,48 @@ export const ChangeModelModal = ({
|
|||||||
variant: PromptVariant;
|
variant: PromptVariant;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const editorOptionsMap = useAppStore((s) => s.sharedVariantEditor.editorOptionsMap);
|
|
||||||
const originalPromptFn = useMemo(
|
|
||||||
() => editorOptionsMap[variant.uiId]?.getContent() || "",
|
|
||||||
[editorOptionsMap, variant.uiId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const originalModel = lookupModel(variant.modelProvider, variant.model);
|
const originalModel = lookupModel(variant.modelProvider, variant.model);
|
||||||
const [selectedModel, setSelectedModel] = useState({
|
const [selectedModel, setSelectedModel] = useState({
|
||||||
provider: variant.modelProvider,
|
provider: variant.modelProvider,
|
||||||
model: variant.model,
|
model: variant.model,
|
||||||
} as ProviderModel);
|
} as ProviderModel);
|
||||||
const [convertedModel, setConvertedModel] = useState<ProviderModel | undefined>();
|
const [convertedModel, setConvertedModel] = useState<ProviderModel | undefined>();
|
||||||
const [modifiedPromptFn, setModifiedPromptFn] = useState<string>();
|
const visibleScenarios = useVisibleScenarioIds();
|
||||||
|
|
||||||
|
const utils = api.useContext();
|
||||||
|
|
||||||
const experiment = useExperiment();
|
const experiment = useExperiment();
|
||||||
|
|
||||||
const { mutateAsync: getModifiedPromptMutateAsync } =
|
const { mutateAsync: getModifiedPromptMutateAsync, data: modifiedPromptFn } =
|
||||||
api.promptVariants.getModifiedPromptFn.useMutation();
|
api.promptVariants.getModifiedPromptFn.useMutation();
|
||||||
|
|
||||||
const [getModifiedPromptFn, modificationInProgress] = useHandledAsyncCallback(async () => {
|
const [getModifiedPromptFn, modificationInProgress] = useHandledAsyncCallback(async () => {
|
||||||
if (!experiment) return;
|
if (!experiment) return;
|
||||||
|
|
||||||
const resp = await getModifiedPromptMutateAsync({
|
await getModifiedPromptMutateAsync({
|
||||||
id: variant.id,
|
id: variant.id,
|
||||||
originalPromptFn,
|
|
||||||
newModel: selectedModel,
|
newModel: selectedModel,
|
||||||
});
|
});
|
||||||
if (maybeReportError(resp)) return;
|
|
||||||
setModifiedPromptFn(resp.payload);
|
|
||||||
setConvertedModel(selectedModel);
|
setConvertedModel(selectedModel);
|
||||||
}, [getModifiedPromptMutateAsync, onClose, experiment, variant, selectedModel]);
|
}, [getModifiedPromptMutateAsync, onClose, experiment, variant, selectedModel]);
|
||||||
|
|
||||||
const replaceVariant = useCallback(() => {
|
const replaceVariantMutation = api.promptVariants.replaceVariant.useMutation();
|
||||||
if (!modifiedPromptFn) return;
|
|
||||||
editorOptionsMap[variant.uiId]?.setContent(modifiedPromptFn);
|
const [replaceVariant, replacementInProgress] = useHandledAsyncCallback(async () => {
|
||||||
|
if (
|
||||||
|
!variant.experimentId ||
|
||||||
|
!modifiedPromptFn ||
|
||||||
|
(isObject(modifiedPromptFn) && "status" in modifiedPromptFn)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
await replaceVariantMutation.mutateAsync({
|
||||||
|
id: variant.id,
|
||||||
|
promptConstructor: modifiedPromptFn,
|
||||||
|
streamScenarios: visibleScenarios,
|
||||||
|
});
|
||||||
|
await utils.promptVariants.list.invalidate();
|
||||||
onClose();
|
onClose();
|
||||||
}, [variant.uiId, editorOptionsMap, onClose, modifiedPromptFn]);
|
}, [replaceVariantMutation, variant, onClose, modifiedPromptFn]);
|
||||||
|
|
||||||
const originalLabel = modelLabel(variant.modelProvider, variant.model);
|
const originalLabel = modelLabel(variant.modelProvider, variant.model);
|
||||||
const selectedLabel = modelLabel(selectedModel.provider, selectedModel.model);
|
const selectedLabel = modelLabel(selectedModel.provider, selectedModel.model);
|
||||||
@@ -128,9 +130,9 @@ export const ChangeModelModal = ({
|
|||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
onClick={replaceVariant}
|
onClick={replaceVariant}
|
||||||
minW={24}
|
minW={24}
|
||||||
isDisabled={!convertedModel || modificationInProgress}
|
isDisabled={!convertedModel || modificationInProgress || replacementInProgress}
|
||||||
>
|
>
|
||||||
Accept
|
{replacementInProgress ? <Spinner boxSize={4} /> : <Text>Accept</Text>}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const ModelStatsCard = ({
|
|||||||
label="Price"
|
label="Price"
|
||||||
info={
|
info={
|
||||||
<Text>
|
<Text>
|
||||||
${model.pricePerSecond.toFixed(4)}
|
${model.pricePerSecond.toFixed(3)}
|
||||||
<Text color="gray.500"> / second</Text>
|
<Text color="gray.500"> / second</Text>
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { HStack, Icon, IconButton, Tooltip, Text, type StackProps } from "@chakra-ui/react";
|
import { HStack, Icon, IconButton, Tooltip, Text } 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, ...rest }: { code: string } & StackProps) => {
|
const CopiableCode = ({ code }: { code: string }) => {
|
||||||
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"
|
||||||
@@ -19,19 +18,9 @@ const CopiableCode = ({ code, ...rest }: { code: string } & StackProps) => {
|
|||||||
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"
|
|
||||||
whiteSpace="pre-wrap"
|
|
||||||
>
|
>
|
||||||
|
<Text fontFamily="inconsolata" fontWeight="bold" letterSpacing={0.5} overflowX="auto">
|
||||||
{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
|
||||||
|
|||||||
74
app/src/components/ExperimentSettingsDrawer/DeleteButton.tsx
Normal file
74
app/src/components/ExperimentSettingsDrawer/DeleteButton.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogBody,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogOverlay,
|
||||||
|
useDisclosure,
|
||||||
|
Text,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { BsTrash } from "react-icons/bs";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
|
||||||
|
export const DeleteButton = () => {
|
||||||
|
const experiment = useExperiment();
|
||||||
|
const mutation = api.experiments.delete.useMutation();
|
||||||
|
const utils = api.useContext();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const closeDrawer = useAppStore((s) => s.closeDrawer);
|
||||||
|
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const [onDeleteConfirm] = useHandledAsyncCallback(async () => {
|
||||||
|
if (!experiment.data?.id) return;
|
||||||
|
await mutation.mutateAsync({ id: experiment.data.id });
|
||||||
|
await utils.experiments.list.invalidate();
|
||||||
|
await router.push({ pathname: "/experiments" });
|
||||||
|
closeDrawer();
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
}, [mutation, experiment.data?.id, router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button size="sm" variant="ghost" colorScheme="red" fontWeight="normal" onClick={onOpen}>
|
||||||
|
<Icon as={BsTrash} boxSize={4} />
|
||||||
|
<Text ml={2}>Delete Experiment</Text>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||||
|
<AlertDialogOverlay>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
|
Delete Experiment
|
||||||
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
<AlertDialogBody>
|
||||||
|
If you delete this experiment all the associated prompts and scenarios will be deleted
|
||||||
|
as well. Are you sure?
|
||||||
|
</AlertDialogBody>
|
||||||
|
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<Button ref={cancelRef} onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button colorScheme="red" onClick={onDeleteConfirm} ml={3}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialogOverlay>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,19 +7,18 @@ import {
|
|||||||
DrawerOverlay,
|
DrawerOverlay,
|
||||||
Heading,
|
Heading,
|
||||||
VStack,
|
VStack,
|
||||||
type UseDisclosureReturn,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import EditScenarioVars from "./EditScenarioVars";
|
import EditScenarioVars from "../OutputsTable/EditScenarioVars";
|
||||||
import EditEvaluations from "./EditEvaluations";
|
import EditEvaluations from "../OutputsTable/EditEvaluations";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
import { DeleteButton } from "./DeleteButton";
|
import { DeleteButton } from "./DeleteButton";
|
||||||
|
|
||||||
export default function ExperimentSettingsDrawer({
|
export default function ExperimentSettingsDrawer() {
|
||||||
disclosure,
|
const isOpen = useAppStore((state) => state.drawerOpen);
|
||||||
}: {
|
const closeDrawer = useAppStore((state) => state.closeDrawer);
|
||||||
disclosure: UseDisclosureReturn;
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<Drawer placement="right" size="md" {...disclosure}>
|
<Drawer isOpen={isOpen} placement="right" onClose={closeDrawer} size="md">
|
||||||
<DrawerOverlay />
|
<DrawerOverlay />
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerCloseButton />
|
<DrawerCloseButton />
|
||||||
@@ -32,7 +31,7 @@ export default function ExperimentSettingsDrawer({
|
|||||||
<EditScenarioVars />
|
<EditScenarioVars />
|
||||||
<EditEvaluations />
|
<EditEvaluations />
|
||||||
</VStack>
|
</VStack>
|
||||||
<DeleteButton closeDrawer={disclosure.onClose} />
|
<DeleteButton />
|
||||||
</VStack>
|
</VStack>
|
||||||
</DrawerBody>
|
</DrawerBody>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
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";
|
|
||||||
import { isEqual } from "lodash-es";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
type InputDropdownProps<T> = {
|
|
||||||
options: ReadonlyArray<T>;
|
|
||||||
selectedOption: T;
|
|
||||||
onSelect: (option: T) => void;
|
|
||||||
inputGroupProps?: InputGroupProps;
|
|
||||||
getDisplayLabel?: (option: T) => string;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const InputDropdown = <T,>({
|
|
||||||
options,
|
|
||||||
selectedOption,
|
|
||||||
onSelect,
|
|
||||||
inputGroupProps,
|
|
||||||
getDisplayLabel = (option) => option as string,
|
|
||||||
isDisabled,
|
|
||||||
}: InputDropdownProps<T>) => {
|
|
||||||
const { onOpen, ...popover } = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover placement="bottom-start" onOpen={isDisabled ? undefined : onOpen} {...popover}>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<InputGroup
|
|
||||||
cursor="pointer"
|
|
||||||
w={getDisplayLabel(selectedOption).length * 14 + 180}
|
|
||||||
{...inputGroupProps}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={getDisplayLabel(selectedOption)}
|
|
||||||
// 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();
|
|
||||||
}}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
/>
|
|
||||||
<InputRightElement>
|
|
||||||
<Icon as={FiChevronDown} color={isDisabled ? "gray.300" : undefined} />
|
|
||||||
</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}>{getDisplayLabel(option)}</Text>
|
|
||||||
{isEqual(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() {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { useCallback, useState } from "react";
|
|||||||
import { BsPencil, BsX } from "react-icons/bs";
|
import { BsPencil, BsX } from "react-icons/bs";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
import AutoResizeTextArea from "~/components/AutoResizeTextArea";
|
import AutoResizeTextArea from "../AutoResizeTextArea";
|
||||||
|
|
||||||
type EvalValues = Pick<Evaluation, "label" | "value" | "evalType">;
|
type EvalValues = Pick<Evaluation, "label" | "value" | "evalType">;
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ import { BsPencil, BsX } from "react-icons/bs";
|
|||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useExperiment, useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks";
|
import { useExperiment, useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks";
|
||||||
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
||||||
import { FloatingLabelInput } from "~/components/OutputsTable/FloatingLabelInput";
|
import { FloatingLabelInput } from "./FloatingLabelInput";
|
||||||
|
|
||||||
export const ScenarioVar = ({
|
export const ScenarioVar = ({
|
||||||
variable,
|
variable,
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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,16 +1,17 @@
|
|||||||
import { Text } from "@chakra-ui/react";
|
import { api } from "~/utils/api";
|
||||||
import stringify from "json-stringify-pretty-compact";
|
import { type PromptVariant, type Scenario } from "../types";
|
||||||
import { Fragment, useEffect, useState, type ReactElement } from "react";
|
import { type StackProps, Text, VStack } from "@chakra-ui/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 frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
import stringify from "json-stringify-pretty-compact";
|
||||||
import { api } from "~/utils/api";
|
import { type ReactElement, useState, useEffect, Fragment, useCallback } from "react";
|
||||||
import { useHandledAsyncCallback, useScenarioVars } from "~/utils/hooks";
|
|
||||||
import useSocket from "~/utils/useSocket";
|
import useSocket from "~/utils/useSocket";
|
||||||
import { type PromptVariant, type Scenario } from "../types";
|
import { OutputStats } from "./OutputStats";
|
||||||
import CellWrapper from "./CellWrapper";
|
|
||||||
import { ResponseLog } from "./ResponseLog";
|
|
||||||
import { RetryCountdown } from "./RetryCountdown";
|
import { RetryCountdown } from "./RetryCountdown";
|
||||||
|
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
||||||
|
import { ResponseLog } from "./ResponseLog";
|
||||||
|
import { CellOptions } from "./TopActions";
|
||||||
|
|
||||||
const WAITING_MESSAGE_INTERVAL = 20000;
|
const WAITING_MESSAGE_INTERVAL = 20000;
|
||||||
|
|
||||||
@@ -32,7 +33,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<number | false>(false);
|
const [refetchInterval, setRefetchInterval] = useState(0);
|
||||||
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 },
|
||||||
@@ -43,7 +44,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
type OutputSchema = Parameters<typeof provider.normalizeOutput>[0];
|
type OutputSchema = Parameters<typeof provider.normalizeOutput>[0];
|
||||||
|
|
||||||
const { mutateAsync: hardRefetchMutate } = api.scenarioVariantCells.hardRefetch.useMutation();
|
const { mutateAsync: hardRefetchMutate } = api.scenarioVariantCells.forceRefetch.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({
|
||||||
@@ -63,34 +64,42 @@ 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 wrapperProps: Parameters<typeof CellWrapper>[0] = {
|
const CellWrapper = useCallback(
|
||||||
cell,
|
({ children, ...props }: StackProps) => (
|
||||||
hardRefetching,
|
<VStack w="full" alignItems="flex-start" {...props} px={2} py={2} h="100%">
|
||||||
hardRefetch,
|
{cell && (
|
||||||
mostRecentResponse,
|
<CellOptions refetchingOutput={hardRefetching} refetchOutput={hardRefetch} cell={cell} />
|
||||||
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 {...wrapperProps}>
|
<CellWrapper>
|
||||||
<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 {...wrapperProps}>
|
<CellWrapper>
|
||||||
<Text color="red.500">{cell.errorMessage}</Text>
|
<Text color="red.500">{cell.errorMessage}</Text>
|
||||||
</CellWrapper>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
@@ -102,12 +111,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (showLogs)
|
if (showLogs)
|
||||||
return (
|
return (
|
||||||
<CellWrapper
|
<CellWrapper alignItems="flex-start" fontFamily="inconsolata, monospace" spacing={0}>
|
||||||
{...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) => {
|
||||||
@@ -116,13 +120,8 @@ export default function OutputCell({
|
|||||||
? response.receivedAt.getTime()
|
? response.receivedAt.getTime()
|
||||||
: Date.now();
|
: Date.now();
|
||||||
if (response.requestedAt) {
|
if (response.requestedAt) {
|
||||||
numWaitingMessages = Math.min(
|
numWaitingMessages = Math.floor(
|
||||||
Math.floor(
|
|
||||||
(relativeWaitingTime - response.requestedAt.getTime()) / WAITING_MESSAGE_INTERVAL,
|
(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 (
|
||||||
@@ -147,10 +146,9 @@ export default function OutputCell({
|
|||||||
<ResponseLog
|
<ResponseLog
|
||||||
time={response.receivedAt}
|
time={response.receivedAt}
|
||||||
title="Response received from API"
|
title="Response received from API"
|
||||||
message={[
|
message={`statusCode: ${response.statusCode ?? ""}\n ${
|
||||||
response.statusCode ? `Status: ${response.statusCode}\n` : "",
|
response.errorMessage ?? ""
|
||||||
response.errorMessage ?? "",
|
}`}
|
||||||
].join("")}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@@ -170,7 +168,7 @@ export default function OutputCell({
|
|||||||
|
|
||||||
if (mostRecentResponse?.respPayload && normalizedOutput?.type === "json") {
|
if (mostRecentResponse?.respPayload && normalizedOutput?.type === "json") {
|
||||||
return (
|
return (
|
||||||
<CellWrapper {...wrapperProps}>
|
<CellWrapper>
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
customStyle={{ overflowX: "unset", width: "100%", flex: 1 }}
|
customStyle={{ overflowX: "unset", width: "100%", flex: 1 }}
|
||||||
language="json"
|
language="json"
|
||||||
@@ -189,7 +187,7 @@ export default function OutputCell({
|
|||||||
const contentToDisplay = (normalizedOutput?.type === "text" && normalizedOutput.value) || "";
|
const contentToDisplay = (normalizedOutput?.type === "text" && normalizedOutput.value) || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellWrapper {...wrapperProps}>
|
<CellWrapper>
|
||||||
<Text whiteSpace="pre-wrap">{contentToDisplay}</Text>
|
<Text whiteSpace="pre-wrap">{contentToDisplay}</Text>
|
||||||
</CellWrapper>
|
</CellWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,103 +5,30 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
VStack,
|
|
||||||
Text,
|
|
||||||
Box,
|
|
||||||
type UseDisclosureReturn,
|
type UseDisclosureReturn,
|
||||||
Link,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { api, type RouterOutputs } from "~/utils/api";
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
import { JSONTree } from "react-json-tree";
|
import { JSONTree } from "react-json-tree";
|
||||||
import CopiableCode from "~/components/CopiableCode";
|
|
||||||
|
|
||||||
const theme = {
|
export default function ExpandedModal(props: {
|
||||||
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="xl">
|
<Modal isOpen={props.disclosure.isOpen} onClose={props.disclosure.onClose} size="2xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>Prompt Details</ModalHeader>
|
<ModalHeader>Prompt</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<VStack py={4} w="">
|
|
||||||
<VStack w="full" alignItems="flex-start">
|
|
||||||
<Text fontWeight="bold">Full Prompt</Text>
|
|
||||||
<Box
|
|
||||||
w="full"
|
|
||||||
p={4}
|
|
||||||
alignItems="flex-start"
|
|
||||||
backgroundColor="blackAlpha.800"
|
|
||||||
borderRadius={4}
|
|
||||||
>
|
|
||||||
<JSONTree
|
<JSONTree
|
||||||
data={props.cell.prompt}
|
data={props.cell.prompt}
|
||||||
theme={theme}
|
invertTheme={true}
|
||||||
|
theme="chalk"
|
||||||
shouldExpandNodeInitially={() => true}
|
shouldExpandNodeInitially={() => true}
|
||||||
getItemString={() => ""}
|
getItemString={() => ""}
|
||||||
hideRoot
|
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>
|
||||||
|
|||||||
@@ -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 PromptModal from "./PromptModal";
|
import ExpandedModal 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>
|
||||||
<PromptModal cell={cell} disclosure={modalDisclosure} />
|
<ExpandedModal cell={cell} disclosure={modalDisclosure} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{canModify && (
|
{canModify && (
|
||||||
@@ -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,11 +111,14 @@ 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>
|
||||||
{variableLabels.length && (
|
|
||||||
<Tooltip label="Expand" hasArrow>
|
<Tooltip label="Expand" hasArrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Expand"
|
aria-label="Expand"
|
||||||
@@ -127,7 +130,6 @@ export default function ScenarioEditor({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
|
||||||
{canModify && props.canHide && (
|
{canModify && props.canHide && (
|
||||||
<Tooltip label="Delete" hasArrow>
|
<Tooltip label="Delete" hasArrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -148,13 +150,7 @@ export default function ScenarioEditor({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
{variableLabels.map((key) => {
|
||||||
{variableLabels.length === 0 ? (
|
|
||||||
<Box color="gray.500">
|
|
||||||
{vars.data ? "No scenario variables configured" : "Loading..."}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
variableLabels.map((key) => {
|
|
||||||
const value = values[key] ?? "";
|
const value = values[key] ?? "";
|
||||||
return (
|
return (
|
||||||
<FloatingLabelInput
|
<FloatingLabelInput
|
||||||
@@ -178,8 +174,7 @@ export default function ScenarioEditor({
|
|||||||
onMouseLeave={() => setVariableInputHovered(false)}
|
onMouseLeave={() => setVariableInputHovered(false)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})}
|
||||||
)}
|
|
||||||
{hasChanged && (
|
{hasChanged && (
|
||||||
<HStack justify="right">
|
<HStack justify="right">
|
||||||
<Button
|
<Button
|
||||||
@@ -197,7 +192,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: "4xl", xl: "5xl" }}
|
size={{ base: "xl", sm: "2xl", md: "3xl", lg: "5xl", xl: "7xl" }}
|
||||||
>
|
>
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent w={1200}>
|
<ModalContent w={1200}>
|
||||||
<ModalHeader>Edit Scenario</ModalHeader>
|
<ModalHeader />
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody maxW="unset">
|
<ModalBody maxW="unset">
|
||||||
<VStack spacing={8}>
|
<VStack spacing={8}>
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -19,13 +19,15 @@ import {
|
|||||||
useScenarios,
|
useScenarios,
|
||||||
} from "~/utils/hooks";
|
} from "~/utils/hooks";
|
||||||
import { BsGear, BsPencil, BsPlus, BsStars } from "react-icons/bs";
|
import { BsGear, BsPencil, BsPlus, BsStars } from "react-icons/bs";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export const ActionButton = (props: ButtonProps) => (
|
export const ActionButton = (props: ButtonProps) => (
|
||||||
<Button size="sm" variant="ghost" color="gray.600" {...props} />
|
<Button size="sm" variant="ghost" color="gray.600" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ScenariosHeader = ({ openDrawer }: { openDrawer: () => void }) => {
|
export const ScenariosHeader = () => {
|
||||||
|
const openDrawer = useAppStore((s) => s.openDrawer);
|
||||||
const { canModify } = useExperimentAccess();
|
const { canModify } = useExperimentAccess();
|
||||||
const scenarios = useScenarios();
|
const scenarios = useScenarios();
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { FiMaximize, FiMinimize } from "react-icons/fi";
|
import { FiMaximize, FiMinimize } from "react-icons/fi";
|
||||||
import { type CreatedEditor, editorBackground } from "~/state/sharedVariantEditor.slice";
|
import { editorBackground } from "~/state/sharedVariantEditor.slice";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import {
|
import {
|
||||||
@@ -24,10 +24,8 @@ import { type PromptVariant } from "./types";
|
|||||||
export default function VariantEditor(props: { variant: PromptVariant }) {
|
export default function VariantEditor(props: { variant: PromptVariant }) {
|
||||||
const { canModify } = useExperimentAccess();
|
const { canModify } = useExperimentAccess();
|
||||||
const monaco = useAppStore.use.sharedVariantEditor.monaco();
|
const monaco = useAppStore.use.sharedVariantEditor.monaco();
|
||||||
const updateOptionsForEditor = useAppStore.use.sharedVariantEditor.updateOptionsForEditor();
|
const editorRef = useRef<ReturnType<NonNullable<typeof monaco>["editor"]["create"]> | null>(null);
|
||||||
const editorRef = useRef<CreatedEditor | null>(null);
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const lastSavedFnRef = useRef(props.variant.promptConstructor);
|
|
||||||
const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`);
|
const [editorId] = useState(() => `editor_${Math.random().toString(36).substring(7)}`);
|
||||||
const [isChanged, setIsChanged] = useState(false);
|
const [isChanged, setIsChanged] = useState(false);
|
||||||
|
|
||||||
@@ -50,18 +48,22 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
}, [isFullscreen, toggleFullscreen]);
|
}, [isFullscreen, toggleFullscreen]);
|
||||||
|
|
||||||
const lastSavedFn = props.variant.promptConstructor;
|
const lastSavedFn = props.variant.promptConstructor;
|
||||||
useEffect(() => {
|
|
||||||
// Store in ref so that we can access it dynamically
|
|
||||||
lastSavedFnRef.current = lastSavedFn;
|
|
||||||
}, [lastSavedFn]);
|
|
||||||
|
|
||||||
const modifierKey = useModifierKeyLabel();
|
const modifierKey = useModifierKeyLabel();
|
||||||
|
|
||||||
const checkForChanges = useCallback(() => {
|
const checkForChanges = useCallback(() => {
|
||||||
if (!editorRef.current) return;
|
if (!editorRef.current) return;
|
||||||
const currentFn = editorRef.current.getValue();
|
const currentFn = editorRef.current.getValue();
|
||||||
setIsChanged(currentFn.length > 0 && currentFn !== lastSavedFnRef.current);
|
setIsChanged(currentFn.length > 0 && currentFn !== lastSavedFn);
|
||||||
}, [editorRef]);
|
}, [lastSavedFn]);
|
||||||
|
|
||||||
|
const matchUpdatedSavedFn = useCallback(() => {
|
||||||
|
if (!editorRef.current) return;
|
||||||
|
editorRef.current.setValue(lastSavedFn);
|
||||||
|
setIsChanged(false);
|
||||||
|
}, [lastSavedFn]);
|
||||||
|
|
||||||
|
useEffect(matchUpdatedSavedFn, [matchUpdatedSavedFn, lastSavedFn]);
|
||||||
|
|
||||||
const replaceVariant = api.promptVariants.replaceVariant.useMutation();
|
const replaceVariant = api.promptVariants.replaceVariant.useMutation();
|
||||||
const utils = api.useContext();
|
const utils = api.useContext();
|
||||||
@@ -108,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, replaceVariant.mutateAsync]);
|
}, [checkForChanges]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (monaco) {
|
if (monaco) {
|
||||||
@@ -134,11 +136,6 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
readOnly: !canModify,
|
readOnly: !canModify,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateOptionsForEditor(props.variant.uiId, {
|
|
||||||
getContent: () => editorRef.current?.getValue() || "",
|
|
||||||
setContent: (content) => editorRef.current?.setValue(content),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Workaround because otherwise the commands only work on whatever
|
// Workaround because otherwise the commands only work on whatever
|
||||||
// editor was loaded on the page last.
|
// editor was loaded on the page last.
|
||||||
// https://github.com/microsoft/monaco-editor/issues/2947#issuecomment-1422265201
|
// https://github.com/microsoft/monaco-editor/issues/2947#issuecomment-1422265201
|
||||||
@@ -158,7 +155,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkForChangesListener = editorRef.current.onDidChangeModelContent(checkForChanges);
|
editorRef.current.onDidChangeModelContent(checkForChanges);
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
editorRef.current?.layout();
|
editorRef.current?.layout();
|
||||||
@@ -167,7 +164,6 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
checkForChangesListener.dispose();
|
|
||||||
editorRef.current?.dispose();
|
editorRef.current?.dispose();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -175,7 +171,7 @@ export default function VariantEditor(props: { variant: PromptVariant }) {
|
|||||||
// We intentionally skip the onSave and props.savedConfig dependencies here because
|
// We intentionally skip the onSave and props.savedConfig dependencies here because
|
||||||
// we don't want to re-render the editor from scratch
|
// we don't want to re-render the editor from scratch
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||||
}, [monaco, editorId, updateOptionsForEditor]);
|
}, [monaco, editorId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editorRef.current) return;
|
if (!editorRef.current) return;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -20,19 +20,15 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
|||||||
inputTokens: 0,
|
inputTokens: 0,
|
||||||
outputTokens: 0,
|
outputTokens: 0,
|
||||||
scenarioCount: 0,
|
scenarioCount: 0,
|
||||||
finishedCount: 0,
|
outputCount: 0,
|
||||||
awaitingCompletions: false,
|
|
||||||
awaitingEvals: false,
|
awaitingEvals: false,
|
||||||
},
|
},
|
||||||
refetchInterval,
|
refetchInterval,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Poll every five seconds while we are waiting for LLM retrievals to finish
|
// Poll every two seconds while we are waiting for LLM retrievals to finish
|
||||||
useEffect(
|
useEffect(() => setRefetchInterval(data.awaitingEvals ? 5000 : 0), [data.awaitingEvals]);
|
||||||
() => 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",
|
||||||
@@ -42,7 +38,7 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
|||||||
|
|
||||||
const scale = chroma.scale([failColor, neutralColor, passColor]).domain([0, 0.5, 1]);
|
const scale = chroma.scale([failColor, neutralColor, passColor]).domain([0, 0.5, 1]);
|
||||||
|
|
||||||
const showNumFinished = data.scenarioCount > 0 && data.scenarioCount !== data.finishedCount;
|
const showNumFinished = data.scenarioCount > 0 && data.scenarioCount !== data.outputCount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
@@ -55,7 +51,7 @@ export default function VariantStats(props: { variant: PromptVariant }) {
|
|||||||
<HStack px={cellPadding.x} flexWrap="wrap">
|
<HStack px={cellPadding.x} flexWrap="wrap">
|
||||||
{showNumFinished && (
|
{showNumFinished && (
|
||||||
<Text>
|
<Text>
|
||||||
{data.finishedCount} / {data.scenarioCount}
|
{data.outputCount} / {data.scenarioCount}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{data.evalResults.map((result) => {
|
{data.evalResults.map((result) => {
|
||||||
|
|||||||
@@ -3,29 +3,21 @@ 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({
|
export default function OutputsTable({ experimentId }: { experimentId: string | undefined }) {
|
||||||
experimentId,
|
|
||||||
openDrawer,
|
|
||||||
}: {
|
|
||||||
experimentId: string | undefined;
|
|
||||||
openDrawer: () => void;
|
|
||||||
}) {
|
|
||||||
const variants = api.promptVariants.list.useQuery(
|
const variants = api.promptVariants.list.useQuery(
|
||||||
{ experimentId: experimentId as string },
|
{ experimentId: experimentId as string },
|
||||||
{ enabled: !!experimentId },
|
{ enabled: !!experimentId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const scenarios = useScenarios();
|
const scenarios = useScenarios();
|
||||||
const shouldFlattenHeader = useScrolledPast(50);
|
|
||||||
|
|
||||||
if (!variants.data || !scenarios.data) return null;
|
if (!variants.data || !scenarios.data) return null;
|
||||||
|
|
||||||
@@ -71,8 +63,8 @@ export default function OutputsTable({
|
|||||||
variant={variant}
|
variant={variant}
|
||||||
canHide={variants.data.length > 1}
|
canHide={variants.data.length > 1}
|
||||||
rowStart={1}
|
rowStart={1}
|
||||||
borderTopLeftRadius={isFirst && !shouldFlattenHeader ? 8 : 0}
|
borderTopLeftRadius={isFirst ? 8 : 0}
|
||||||
borderTopRightRadius={isLast && !shouldFlattenHeader ? 8 : 0}
|
borderTopRightRadius={isLast ? 8 : 0}
|
||||||
{...sharedProps}
|
{...sharedProps}
|
||||||
/>
|
/>
|
||||||
<GridItem rowStart={2} {...sharedProps}>
|
<GridItem rowStart={2} {...sharedProps}>
|
||||||
@@ -83,7 +75,6 @@ export default function OutputsTable({
|
|||||||
{...sharedProps}
|
{...sharedProps}
|
||||||
borderBottomLeftRadius={isFirst ? 8 : 0}
|
borderBottomLeftRadius={isFirst ? 8 : 0}
|
||||||
borderBottomRightRadius={isLast ? 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>
|
||||||
@@ -97,7 +88,7 @@ export default function OutputsTable({
|
|||||||
colStart={1}
|
colStart={1}
|
||||||
borderRightWidth={0}
|
borderRightWidth={0}
|
||||||
>
|
>
|
||||||
<ScenariosHeader openDrawer={openDrawer} />
|
<ScenariosHeader />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
|
|
||||||
{scenarios.data.scenarios.map((scenario, i) => (
|
{scenarios.data.scenarios.map((scenario, i) => (
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
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,19 +1,15 @@
|
|||||||
import {
|
import { HStack, IconButton, Text, Select, type StackProps, Icon } from "@chakra-ui/react";
|
||||||
HStack,
|
|
||||||
IconButton,
|
|
||||||
Text,
|
|
||||||
Select,
|
|
||||||
type StackProps,
|
|
||||||
Icon,
|
|
||||||
useBreakpointValue,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { FiChevronsLeft, FiChevronsRight, FiChevronLeft, FiChevronRight } from "react-icons/fi";
|
import { FiChevronsLeft, FiChevronsRight, FiChevronLeft, FiChevronRight } from "react-icons/fi";
|
||||||
import { usePageParams } from "~/utils/hooks";
|
import { usePageParams } from "~/utils/hooks";
|
||||||
|
|
||||||
const pageSizeOptions = [10, 25, 50, 100];
|
const pageSizeOptions = [10, 25, 50, 100];
|
||||||
|
|
||||||
const Paginator = ({ count, ...props }: { count: number; condense?: boolean } & StackProps) => {
|
const Paginator = ({
|
||||||
|
count,
|
||||||
|
condense,
|
||||||
|
...props
|
||||||
|
}: { count: number; condense?: boolean } & StackProps) => {
|
||||||
const { page, pageSize, setPageParams } = usePageParams();
|
const { page, pageSize, setPageParams } = usePageParams();
|
||||||
|
|
||||||
const lastPage = Math.ceil(count / pageSize);
|
const lastPage = Math.ceil(count / pageSize);
|
||||||
@@ -41,11 +37,6 @@ const Paginator = ({ count, ...props }: { count: number; condense?: boolean } &
|
|||||||
const goToLastPage = () => setPageParams({ page: lastPage }, "replace");
|
const goToLastPage = () => setPageParams({ page: lastPage }, "replace");
|
||||||
const goToFirstPage = () => setPageParams({ page: 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
|
<HStack
|
||||||
pt={4}
|
pt={4}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useState, useMemo, useCallback } from "react";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -10,23 +9,22 @@ import {
|
|||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
VStack,
|
VStack,
|
||||||
Text,
|
Text,
|
||||||
|
Spinner,
|
||||||
HStack,
|
HStack,
|
||||||
Icon,
|
Icon,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { BsStars } from "react-icons/bs";
|
import { BsStars } from "react-icons/bs";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
import { useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
||||||
import { type PromptVariant } from "@prisma/client";
|
import { type PromptVariant } from "@prisma/client";
|
||||||
|
import { useState } from "react";
|
||||||
import CompareFunctions from "./CompareFunctions";
|
import CompareFunctions from "./CompareFunctions";
|
||||||
import { CustomInstructionsInput } from "../CustomInstructionsInput";
|
import { CustomInstructionsInput } from "../CustomInstructionsInput";
|
||||||
import { RefineAction } from "./RefineAction";
|
import { RefineAction } from "./RefineAction";
|
||||||
import { isString } from "lodash-es";
|
import { isObject, isString } from "lodash-es";
|
||||||
import { type RefinementAction, type SupportedProvider } from "~/modelProviders/types";
|
import { type RefinementAction, type SupportedProvider } from "~/modelProviders/types";
|
||||||
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
|
||||||
|
|
||||||
export const RefinePromptModal = ({
|
export const RefinePromptModal = ({
|
||||||
variant,
|
variant,
|
||||||
@@ -35,23 +33,19 @@ export const RefinePromptModal = ({
|
|||||||
variant: PromptVariant;
|
variant: PromptVariant;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const editorOptionsMap = useAppStore((s) => s.sharedVariantEditor.editorOptionsMap);
|
const utils = api.useContext();
|
||||||
const originalPromptFn = useMemo(
|
const visibleScenarios = useVisibleScenarioIds();
|
||||||
() => editorOptionsMap[variant.uiId]?.getContent() || "",
|
|
||||||
[editorOptionsMap, variant.uiId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const refinementActions =
|
const refinementActions =
|
||||||
frontendModelProviders[variant.modelProvider as SupportedProvider].refinementActions || {};
|
frontendModelProviders[variant.modelProvider as SupportedProvider].refinementActions || {};
|
||||||
|
|
||||||
const { mutateAsync: getModifiedPromptMutateAsync } =
|
const { mutateAsync: getModifiedPromptMutateAsync, data: refinedPromptFn } =
|
||||||
api.promptVariants.getModifiedPromptFn.useMutation();
|
api.promptVariants.getModifiedPromptFn.useMutation();
|
||||||
const [instructions, setInstructions] = useState<string>("");
|
const [instructions, setInstructions] = useState<string>("");
|
||||||
|
|
||||||
const [activeRefineActionLabel, setActiveRefineActionLabel] = useState<string | undefined>(
|
const [activeRefineActionLabel, setActiveRefineActionLabel] = useState<string | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
const [refinedPromptFn, setRefinedPromptFn] = useState<string>();
|
|
||||||
|
|
||||||
const [getModifiedPromptFn, modificationInProgress] = useHandledAsyncCallback(
|
const [getModifiedPromptFn, modificationInProgress] = useHandledAsyncCallback(
|
||||||
async (label?: string) => {
|
async (label?: string) => {
|
||||||
@@ -60,22 +54,31 @@ export const RefinePromptModal = ({
|
|||||||
? (refinementActions[label] as RefinementAction).instructions
|
? (refinementActions[label] as RefinementAction).instructions
|
||||||
: instructions;
|
: instructions;
|
||||||
setActiveRefineActionLabel(label);
|
setActiveRefineActionLabel(label);
|
||||||
const resp = await getModifiedPromptMutateAsync({
|
await getModifiedPromptMutateAsync({
|
||||||
id: variant.id,
|
id: variant.id,
|
||||||
originalPromptFn,
|
|
||||||
instructions: updatedInstructions,
|
instructions: updatedInstructions,
|
||||||
});
|
});
|
||||||
if (maybeReportError(resp)) return;
|
|
||||||
setRefinedPromptFn(resp.payload);
|
|
||||||
},
|
},
|
||||||
[getModifiedPromptMutateAsync, onClose, variant, instructions, setActiveRefineActionLabel],
|
[getModifiedPromptMutateAsync, onClose, variant, instructions, setActiveRefineActionLabel],
|
||||||
);
|
);
|
||||||
|
|
||||||
const replaceVariant = useCallback(() => {
|
const replaceVariantMutation = api.promptVariants.replaceVariant.useMutation();
|
||||||
if (!refinedPromptFn) return;
|
|
||||||
editorOptionsMap[variant.uiId]?.setContent(refinedPromptFn);
|
const [replaceVariant, replacementInProgress] = useHandledAsyncCallback(async () => {
|
||||||
|
if (
|
||||||
|
!variant.experimentId ||
|
||||||
|
!refinedPromptFn ||
|
||||||
|
(isObject(refinedPromptFn) && "status" in refinedPromptFn)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
await replaceVariantMutation.mutateAsync({
|
||||||
|
id: variant.id,
|
||||||
|
promptConstructor: refinedPromptFn,
|
||||||
|
streamScenarios: visibleScenarios,
|
||||||
|
});
|
||||||
|
await utils.promptVariants.list.invalidate();
|
||||||
onClose();
|
onClose();
|
||||||
}, [variant.uiId, editorOptionsMap, onClose, refinedPromptFn]);
|
}, [replaceVariantMutation, variant, onClose, refinedPromptFn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -123,7 +126,7 @@ export const RefinePromptModal = ({
|
|||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
<CompareFunctions
|
<CompareFunctions
|
||||||
originalFunction={originalPromptFn}
|
originalFunction={variant.promptConstructor}
|
||||||
newFunction={isString(refinedPromptFn) ? refinedPromptFn : undefined}
|
newFunction={isString(refinedPromptFn) ? refinedPromptFn : undefined}
|
||||||
maxH="40vh"
|
maxH="40vh"
|
||||||
/>
|
/>
|
||||||
@@ -136,9 +139,9 @@ export const RefinePromptModal = ({
|
|||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
onClick={replaceVariant}
|
onClick={replaceVariant}
|
||||||
minW={24}
|
minW={24}
|
||||||
isDisabled={!refinedPromptFn}
|
isDisabled={replacementInProgress || !refinedPromptFn}
|
||||||
>
|
>
|
||||||
Accept
|
{replacementInProgress ? <Spinner boxSize={4} /> : <Text>Accept</Text>}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
26
app/src/components/StatsCard.tsx
Normal file
26
app/src/components/StatsCard.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { VStack, HStack, type StackProps, Text, Divider } from "@chakra-ui/react";
|
||||||
|
import Link, { type LinkProps } from "next/link";
|
||||||
|
|
||||||
|
const StatsCard = ({
|
||||||
|
title,
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}: { title: string; href: string } & StackProps & LinkProps) => {
|
||||||
|
return (
|
||||||
|
<VStack flex={1} borderWidth={1} padding={4} borderRadius={4} borderColor="gray.300" {...rest}>
|
||||||
|
<HStack w="full" justifyContent="space-between">
|
||||||
|
<Text fontSize="md" fontWeight="bold">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Link href={href}>
|
||||||
|
<Text color="blue">View all</Text>
|
||||||
|
</Link>
|
||||||
|
</HStack>
|
||||||
|
<Divider />
|
||||||
|
{children}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatsCard;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState, type DragEvent } from "react";
|
import { useState, type DragEvent } from "react";
|
||||||
import { type PromptVariant } from "../types";
|
import { type PromptVariant } from "../OutputsTable/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(
|
||||||
@@ -75,7 +75,7 @@ export default function VariantHeader(
|
|||||||
padding={0}
|
padding={0}
|
||||||
sx={{
|
sx={{
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
top: "0",
|
top: "-2",
|
||||||
// Ensure that the menu always appears above the sticky header of other variants
|
// Ensure that the menu always appears above the sticky header of other variants
|
||||||
zIndex: menuOpen ? "dropdown" : 10,
|
zIndex: menuOpen ? "dropdown" : 10,
|
||||||
}}
|
}}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { type PromptVariant } from "../OutputsTable/types";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { useHandledAsyncCallback, useVisibleScenarioIds } from "~/utils/hooks";
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -12,13 +14,10 @@ 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,
|
||||||
@@ -2,12 +2,11 @@ import { Card, CardHeader, Heading, Table, Tbody, HStack, Button, Text } from "@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useLoggedCalls } from "~/utils/hooks";
|
import { useLoggedCalls } from "~/utils/hooks";
|
||||||
import { EmptyTableRow, TableHeader, TableRow } from "../requestLogs/TableRow";
|
import { TableHeader, TableRow } from "../requestLogs/TableRow";
|
||||||
|
|
||||||
export default function LoggedCallsTable() {
|
export default function LoggedCallsTable() {
|
||||||
const { data: loggedCalls } = useLoggedCalls(false);
|
|
||||||
|
|
||||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||||
|
const { data: loggedCalls } = useLoggedCalls();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card width="100%" overflow="hidden">
|
<Card width="100%" overflow="hidden">
|
||||||
@@ -24,8 +23,7 @@ export default function LoggedCallsTable() {
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader />
|
<TableHeader />
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{loggedCalls?.calls.length ? (
|
{loggedCalls?.calls.map((loggedCall) => {
|
||||||
loggedCalls?.calls.map((loggedCall) => {
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={loggedCall.id}
|
key={loggedCall.id}
|
||||||
@@ -40,10 +38,7 @@ export default function LoggedCallsTable() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})}
|
||||||
) : (
|
|
||||||
<EmptyTableRow filtersApplied={false} />
|
|
||||||
)}
|
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
112
app/src/components/datasets/DatasetCard.tsx
Normal file
112
app/src/components/datasets/DatasetCard.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
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,37 +0,0 @@
|
|||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerBody,
|
|
||||||
DrawerCloseButton,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerOverlay,
|
|
||||||
Heading,
|
|
||||||
VStack,
|
|
||||||
type UseDisclosureReturn,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
|
|
||||||
import { DeleteButton } from "./DeleteButton";
|
|
||||||
|
|
||||||
export default function DatasetConfigurationDrawer({
|
|
||||||
disclosure,
|
|
||||||
}: {
|
|
||||||
disclosure: UseDisclosureReturn;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Drawer placement="right" size="md" {...disclosure}>
|
|
||||||
<DrawerOverlay />
|
|
||||||
<DrawerContent>
|
|
||||||
<DrawerCloseButton />
|
|
||||||
<DrawerHeader>
|
|
||||||
<Heading size="md">Dataset Configuration</Heading>
|
|
||||||
</DrawerHeader>
|
|
||||||
<DrawerBody h="full" pb={4}>
|
|
||||||
<VStack h="full" justifyContent="space-between">
|
|
||||||
<VStack spacing={6}></VStack>
|
|
||||||
<DeleteButton closeDrawer={disclosure.onClose} />
|
|
||||||
</VStack>
|
|
||||||
</DrawerBody>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Button, Icon, useDisclosure, Text } from "@chakra-ui/react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { BsTrash } from "react-icons/bs";
|
|
||||||
|
|
||||||
import { useHandledAsyncCallback, useDataset } from "~/utils/hooks";
|
|
||||||
import DeleteDatasetDialog from "./DeleteDatasetDialog";
|
|
||||||
|
|
||||||
export const DeleteButton = ({ closeDrawer }: { closeDrawer: () => void }) => {
|
|
||||||
const dataset = useDataset();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const disclosure = useDisclosure();
|
|
||||||
|
|
||||||
const [onDelete] = useHandledAsyncCallback(async () => {
|
|
||||||
await router.push({ pathname: "/datasets" });
|
|
||||||
closeDrawer();
|
|
||||||
}, [router, closeDrawer]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
colorScheme="red"
|
|
||||||
fontWeight="normal"
|
|
||||||
onClick={disclosure.onOpen}
|
|
||||||
>
|
|
||||||
<Icon as={BsTrash} boxSize={4} />
|
|
||||||
<Text ml={2}>Delete Dataset</Text>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<DeleteDatasetDialog
|
|
||||||
datasetId={dataset.data?.id}
|
|
||||||
onDelete={onDelete}
|
|
||||||
disclosure={disclosure}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import { useRef } from "react";
|
|
||||||
import {
|
|
||||||
type UseDisclosureReturn,
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogBody,
|
|
||||||
AlertDialogFooter,
|
|
||||||
Button,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
|
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
|
|
||||||
const DeleteDatasetDialog = ({
|
|
||||||
datasetId,
|
|
||||||
onDelete,
|
|
||||||
disclosure,
|
|
||||||
}: {
|
|
||||||
datasetId?: string;
|
|
||||||
onDelete?: () => void;
|
|
||||||
disclosure: UseDisclosureReturn;
|
|
||||||
}) => {
|
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
const mutation = api.datasets.delete.useMutation();
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const [onDeleteConfirm, deletionInProgress] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!datasetId) return;
|
|
||||||
await mutation.mutateAsync({ id: datasetId });
|
|
||||||
await utils.datasets.list.invalidate();
|
|
||||||
onDelete?.();
|
|
||||||
|
|
||||||
disclosure.onClose();
|
|
||||||
}, [mutation, datasetId, disclosure.onClose]);
|
|
||||||
|
|
||||||
console.log("dataset id", datasetId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertDialog leastDestructiveRef={cancelRef} {...disclosure}>
|
|
||||||
<AlertDialogOverlay>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
|
||||||
Delete Dataset
|
|
||||||
</AlertDialogHeader>
|
|
||||||
|
|
||||||
<AlertDialogBody>
|
|
||||||
If you delete this dataset all the associated dataset entries will be deleted as well.
|
|
||||||
Are you sure?
|
|
||||||
</AlertDialogBody>
|
|
||||||
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<Button ref={cancelRef} onClick={disclosure.onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorScheme="red"
|
|
||||||
isLoading={deletionInProgress}
|
|
||||||
onClick={onDeleteConfirm}
|
|
||||||
ml={3}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialogOverlay>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeleteDatasetDialog;
|
|
||||||
@@ -3,14 +3,14 @@ import { type StackProps } from "@chakra-ui/react";
|
|||||||
import { useDatasetEntries } from "~/utils/hooks";
|
import { useDatasetEntries } from "~/utils/hooks";
|
||||||
import Paginator from "../Paginator";
|
import Paginator from "../Paginator";
|
||||||
|
|
||||||
const DatasetEntryPaginator = (props: StackProps) => {
|
const DatasetEntriesPaginator = (props: StackProps) => {
|
||||||
const { data } = useDatasetEntries();
|
const { data } = useDatasetEntries();
|
||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const { matchingEntryIds } = data;
|
const { count } = data;
|
||||||
|
|
||||||
return <Paginator count={matchingEntryIds.length} {...props} />;
|
return <Paginator count={count} {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DatasetEntryPaginator;
|
export default DatasetEntriesPaginator;
|
||||||
31
app/src/components/datasets/DatasetEntriesTable.tsx
Normal file
31
app/src/components/datasets/DatasetEntriesTable.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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,46 +0,0 @@
|
|||||||
import { Card, Table, Tbody } from "@chakra-ui/react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useDatasetEntries } from "~/utils/hooks";
|
|
||||||
import { TableHeader, TableRow, EmptyTableRow } from "./TableRow";
|
|
||||||
import DatasetEntryEditorDrawer from "./DatasetEntryEditorDrawer";
|
|
||||||
|
|
||||||
export default function DatasetEntriesTable() {
|
|
||||||
const [expandedDatasetEntryId, setExpandedDatasetEntryId] = useState<string | null>(null);
|
|
||||||
const datasetEntries = useDatasetEntries().data?.entries;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card width="100%" overflowX="auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader />
|
|
||||||
<Tbody>
|
|
||||||
{datasetEntries?.length ? (
|
|
||||||
datasetEntries?.map((entry) => {
|
|
||||||
return (
|
|
||||||
<TableRow
|
|
||||||
key={entry.id}
|
|
||||||
datasetEntry={entry}
|
|
||||||
onToggle={() => {
|
|
||||||
if (entry.id === expandedDatasetEntryId) {
|
|
||||||
setExpandedDatasetEntryId(null);
|
|
||||||
} else {
|
|
||||||
setExpandedDatasetEntryId(entry.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
showOptions
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<EmptyTableRow />
|
|
||||||
)}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
<DatasetEntryEditorDrawer
|
|
||||||
datasetEntryId={expandedDatasetEntryId}
|
|
||||||
clearDatasetEntryId={() => setExpandedDatasetEntryId(null)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
DrawerBody,
|
|
||||||
DrawerCloseButton,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerOverlay,
|
|
||||||
DrawerFooter,
|
|
||||||
Heading,
|
|
||||||
VStack,
|
|
||||||
HStack,
|
|
||||||
Button,
|
|
||||||
Text,
|
|
||||||
Divider,
|
|
||||||
Icon,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { type CreateChatCompletionRequestMessage } from "openai/resources/chat";
|
|
||||||
import { BsPlus } from "react-icons/bs";
|
|
||||||
import { type DatasetEntryType } from "@prisma/client";
|
|
||||||
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useDatasetEntry, useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import EditableMessage from "./EditableMessage";
|
|
||||||
import EntryTypeDropdown from "./EntryTypeDropdown";
|
|
||||||
|
|
||||||
export default function DatasetDentryEditorDrawer({
|
|
||||||
datasetEntryId,
|
|
||||||
clearDatasetEntryId,
|
|
||||||
}: {
|
|
||||||
datasetEntryId: string | null;
|
|
||||||
clearDatasetEntryId: () => void;
|
|
||||||
}) {
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const datasetEntry = useDatasetEntry(datasetEntryId).data;
|
|
||||||
|
|
||||||
const savedInputMessages = useMemo(
|
|
||||||
() => datasetEntry?.input as unknown as CreateChatCompletionRequestMessage[],
|
|
||||||
[datasetEntry],
|
|
||||||
);
|
|
||||||
const savedOutputMessage = useMemo(
|
|
||||||
() => datasetEntry?.output as unknown as CreateChatCompletionRequestMessage,
|
|
||||||
[datasetEntry],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [inputMessagesToSave, setInputMessagesToSave] = useState<
|
|
||||||
CreateChatCompletionRequestMessage[]
|
|
||||||
>([]);
|
|
||||||
const [outputMessageToSave, setOutputMessageToSave] =
|
|
||||||
useState<CreateChatCompletionRequestMessage | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (savedInputMessages) {
|
|
||||||
setInputMessagesToSave(savedInputMessages);
|
|
||||||
setOutputMessageToSave(savedOutputMessage);
|
|
||||||
}
|
|
||||||
}, [savedInputMessages, savedOutputMessage]);
|
|
||||||
|
|
||||||
const updateMutation = api.datasetEntries.update.useMutation();
|
|
||||||
const [onSave, savingInProgress] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!datasetEntryId || !inputMessagesToSave) return;
|
|
||||||
await updateMutation.mutateAsync({
|
|
||||||
id: datasetEntryId,
|
|
||||||
updates: {
|
|
||||||
input: JSON.stringify(inputMessagesToSave),
|
|
||||||
output: JSON.stringify(outputMessageToSave),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await utils.datasetEntries.list.invalidate();
|
|
||||||
await utils.datasetEntries.get.invalidate({ id: datasetEntryId });
|
|
||||||
}, [updateMutation, datasetEntryId, inputMessagesToSave, outputMessageToSave, utils]);
|
|
||||||
|
|
||||||
const [onUpdateType] = useHandledAsyncCallback(
|
|
||||||
async (type: DatasetEntryType) => {
|
|
||||||
if (!datasetEntryId) return;
|
|
||||||
await updateMutation.mutateAsync({
|
|
||||||
id: datasetEntryId,
|
|
||||||
updates: {
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await utils.datasetEntries.list.invalidate();
|
|
||||||
await utils.datasetEntries.get.invalidate({ id: datasetEntryId });
|
|
||||||
},
|
|
||||||
[updateMutation, datasetEntryId, utils],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer isOpen={!!datasetEntryId} onClose={clearDatasetEntryId} placement="right" size="md">
|
|
||||||
<DrawerOverlay />
|
|
||||||
<DrawerContent>
|
|
||||||
<DrawerCloseButton pt={6} />
|
|
||||||
<DrawerHeader bgColor="orange.50">
|
|
||||||
<HStack w="full" justifyContent="space-between" pr={8}>
|
|
||||||
<Heading size="md">Dataset Entry</Heading>
|
|
||||||
{datasetEntry && (
|
|
||||||
<EntryTypeDropdown type={datasetEntry.type} onTypeChange={onUpdateType} />
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</DrawerHeader>
|
|
||||||
<DrawerBody h="full" pb={4} bgColor="orange.50">
|
|
||||||
<VStack h="full" justifyContent="space-between">
|
|
||||||
<VStack w="full" spacing={12} py={4}>
|
|
||||||
<VStack w="full" alignItems="flex-start">
|
|
||||||
<Text fontWeight="bold">Input</Text>
|
|
||||||
{inputMessagesToSave.map((message, i) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Divider key={`divider-${i}`} my={4} />
|
|
||||||
<EditableMessage
|
|
||||||
key={i}
|
|
||||||
message={message}
|
|
||||||
onEdit={(message) => {
|
|
||||||
const newInputMessages = [...inputMessagesToSave];
|
|
||||||
newInputMessages[i] = message;
|
|
||||||
setInputMessagesToSave(newInputMessages);
|
|
||||||
}}
|
|
||||||
onDelete={() => {
|
|
||||||
const newInputMessages = [...inputMessagesToSave];
|
|
||||||
newInputMessages.splice(i, 1);
|
|
||||||
setInputMessagesToSave(newInputMessages);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<Divider my={4} />
|
|
||||||
<Button
|
|
||||||
w="full"
|
|
||||||
onClick={() =>
|
|
||||||
setInputMessagesToSave([...inputMessagesToSave, { role: "user", content: "" }])
|
|
||||||
}
|
|
||||||
variant="outline"
|
|
||||||
color="gray.500"
|
|
||||||
_hover={{ bgColor: "orange.100" }}
|
|
||||||
>
|
|
||||||
<HStack spacing={0}>
|
|
||||||
<Text>Add Message</Text>
|
|
||||||
<Icon as={BsPlus} boxSize={6} />
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
</VStack>
|
|
||||||
<VStack w="full" alignItems="flex-start">
|
|
||||||
<Text fontWeight="bold">Output</Text>
|
|
||||||
<Divider my={4} />
|
|
||||||
<EditableMessage
|
|
||||||
message={outputMessageToSave}
|
|
||||||
onEdit={(message) => setOutputMessageToSave(message)}
|
|
||||||
isOutput
|
|
||||||
/>
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
</DrawerBody>
|
|
||||||
<DrawerFooter bgColor="orange.50">
|
|
||||||
<HStack>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setInputMessagesToSave(savedInputMessages);
|
|
||||||
setOutputMessageToSave(savedOutputMessage);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</Button>
|
|
||||||
<Button isLoading={savingInProgress} onClick={onSave} colorScheme="orange">
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</DrawerFooter>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import { VStack, HStack, Tooltip, IconButton, Icon } from "@chakra-ui/react";
|
|
||||||
import { type CreateChatCompletionRequestMessage } from "openai/resources/chat";
|
|
||||||
import { BsX } from "react-icons/bs";
|
|
||||||
|
|
||||||
import AutoResizeTextArea from "~/components/AutoResizeTextArea";
|
|
||||||
import InputDropdown from "~/components/InputDropdown";
|
|
||||||
import { parseableToFunctionCall } from "~/utils/utils";
|
|
||||||
import FunctionCallEditor from "./FunctionCallEditor";
|
|
||||||
|
|
||||||
const MESSAGE_ROLE_OPTIONS = ["system", "user", "assistant", "function"] as const;
|
|
||||||
const OUTPUT_OPTIONS = ["plaintext", "func_call"] as const;
|
|
||||||
|
|
||||||
const EditableMessage = ({
|
|
||||||
message,
|
|
||||||
onEdit,
|
|
||||||
onDelete,
|
|
||||||
isOutput,
|
|
||||||
}: {
|
|
||||||
message: CreateChatCompletionRequestMessage | null;
|
|
||||||
onEdit: (message: CreateChatCompletionRequestMessage) => void;
|
|
||||||
onDelete?: () => void;
|
|
||||||
isOutput?: boolean;
|
|
||||||
}) => {
|
|
||||||
const { role = "assistant", content = "", function_call } = message || {};
|
|
||||||
|
|
||||||
const currentOutputOption: (typeof OUTPUT_OPTIONS)[number] = function_call
|
|
||||||
? "func_call"
|
|
||||||
: "plaintext";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack w="full">
|
|
||||||
<HStack w="full" justifyContent="space-between">
|
|
||||||
<HStack>
|
|
||||||
{!isOutput && (
|
|
||||||
<InputDropdown
|
|
||||||
options={MESSAGE_ROLE_OPTIONS}
|
|
||||||
selectedOption={role}
|
|
||||||
onSelect={(option) => {
|
|
||||||
const updatedMessage = { role: option, content };
|
|
||||||
if (role === "assistant" && currentOutputOption === "func_call") {
|
|
||||||
updatedMessage.content = JSON.stringify(function_call, null, 2);
|
|
||||||
}
|
|
||||||
onEdit(updatedMessage);
|
|
||||||
}}
|
|
||||||
inputGroupProps={{ w: "32", bgColor: "white" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{role === "assistant" && (
|
|
||||||
<InputDropdown
|
|
||||||
options={OUTPUT_OPTIONS}
|
|
||||||
selectedOption={currentOutputOption}
|
|
||||||
onSelect={(option) => {
|
|
||||||
const updatedMessage: CreateChatCompletionRequestMessage = {
|
|
||||||
role,
|
|
||||||
content: null,
|
|
||||||
function_call: undefined,
|
|
||||||
};
|
|
||||||
if (option === "plaintext") {
|
|
||||||
updatedMessage.content = JSON.stringify(function_call, null, 2);
|
|
||||||
} else if (option === "func_call") {
|
|
||||||
updatedMessage.function_call =
|
|
||||||
content && parseableToFunctionCall(content)
|
|
||||||
? JSON.parse(content)
|
|
||||||
: { name: "", arguments: "{}" };
|
|
||||||
}
|
|
||||||
onEdit(updatedMessage);
|
|
||||||
}}
|
|
||||||
inputGroupProps={{ w: "32", bgColor: "white" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
{!isOutput && (
|
|
||||||
<HStack>
|
|
||||||
<Tooltip label="Delete" hasArrow>
|
|
||||||
<IconButton
|
|
||||||
aria-label="Delete"
|
|
||||||
icon={<Icon as={BsX} boxSize={6} />}
|
|
||||||
onClick={onDelete}
|
|
||||||
size="xs"
|
|
||||||
display="flex"
|
|
||||||
colorScheme="gray"
|
|
||||||
color="gray.500"
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</HStack>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
{function_call ? (
|
|
||||||
<FunctionCallEditor
|
|
||||||
function_call={function_call}
|
|
||||||
onEdit={(function_call) => onEdit({ role, function_call, content: null })}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<AutoResizeTextArea
|
|
||||||
value={content || JSON.stringify(function_call, null, 2)}
|
|
||||||
onChange={(e) => onEdit({ role, content: e.target.value })}
|
|
||||||
bgColor="white"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditableMessage;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { type DatasetEntryType } from "@prisma/client";
|
|
||||||
|
|
||||||
import InputDropdown from "~/components/InputDropdown";
|
|
||||||
|
|
||||||
const ENTRY_TYPE_OPTIONS: DatasetEntryType[] = ["TRAIN", "TEST"];
|
|
||||||
|
|
||||||
const EntryTypeDropdown = ({
|
|
||||||
type,
|
|
||||||
onTypeChange,
|
|
||||||
}: {
|
|
||||||
type: DatasetEntryType;
|
|
||||||
onTypeChange: (type: DatasetEntryType) => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<InputDropdown
|
|
||||||
options={ENTRY_TYPE_OPTIONS}
|
|
||||||
selectedOption={type}
|
|
||||||
onSelect={onTypeChange}
|
|
||||||
inputGroupProps={{ w: "32", bgColor: "white" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EntryTypeDropdown;
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { useRef, useMemo, useEffect } from "react";
|
|
||||||
import { VStack, HStack, Text, Input, Box } from "@chakra-ui/react";
|
|
||||||
import { type CreateChatCompletionRequestMessage } from "openai/resources/chat";
|
|
||||||
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import { type CreatedEditor } from "~/state/sharedVariantEditor.slice";
|
|
||||||
|
|
||||||
const FunctionCallEditor = ({
|
|
||||||
function_call,
|
|
||||||
onEdit,
|
|
||||||
}: {
|
|
||||||
function_call: CreateChatCompletionRequestMessage.FunctionCall;
|
|
||||||
onEdit: (function_call: CreateChatCompletionRequestMessage.FunctionCall) => void;
|
|
||||||
}) => {
|
|
||||||
const monaco = useAppStore.use.sharedArgumentsEditor.monaco();
|
|
||||||
const editorRef = useRef<CreatedEditor | null>(null);
|
|
||||||
const editorId = useMemo(() => `editor_${Math.random().toString(36).substring(7)}`, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (monaco) {
|
|
||||||
const container = document.getElementById(editorId) as HTMLElement;
|
|
||||||
|
|
||||||
const editor = monaco.editor.create(container, {
|
|
||||||
value: function_call.arguments,
|
|
||||||
language: "json",
|
|
||||||
theme: "customTheme",
|
|
||||||
lineNumbers: "off",
|
|
||||||
minimap: { enabled: false },
|
|
||||||
wrappingIndent: "indent",
|
|
||||||
wrappingStrategy: "advanced",
|
|
||||||
wordWrap: "on",
|
|
||||||
folding: false,
|
|
||||||
scrollbar: {
|
|
||||||
alwaysConsumeMouseWheel: false,
|
|
||||||
verticalScrollbarSize: 0,
|
|
||||||
},
|
|
||||||
wordWrapBreakAfterCharacters: "",
|
|
||||||
wordWrapBreakBeforeCharacters: "",
|
|
||||||
quickSuggestions: true,
|
|
||||||
renderLineHighlight: "none",
|
|
||||||
fontSize: 14,
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
editorRef.current = editor;
|
|
||||||
|
|
||||||
const updateHeight = () => {
|
|
||||||
const contentHeight = editor.getContentHeight();
|
|
||||||
container.style.height = `${contentHeight}px`;
|
|
||||||
editor.layout();
|
|
||||||
};
|
|
||||||
|
|
||||||
const attemptDocumentFormat = () => {
|
|
||||||
const action = editor.getAction("editor.action.formatDocument");
|
|
||||||
if (action) {
|
|
||||||
action
|
|
||||||
.run()
|
|
||||||
.then(updateHeight)
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Error running formatDocument:", error);
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
editor.onDidBlurEditorText(() => {
|
|
||||||
attemptDocumentFormat();
|
|
||||||
onEdit({ name: function_call.name, arguments: editor.getValue() });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Interval function to check for action availability
|
|
||||||
const checkForActionInterval = setInterval(() => {
|
|
||||||
const formatted = attemptDocumentFormat();
|
|
||||||
if (formatted) {
|
|
||||||
clearInterval(checkForActionInterval); // Clear the interval once the action is found and run
|
|
||||||
}
|
|
||||||
}, 100); // Check every 100ms
|
|
||||||
|
|
||||||
// Add content change listener
|
|
||||||
const contentChangeListener = editor.onDidChangeModelContent(updateHeight);
|
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
|
||||||
editor.layout();
|
|
||||||
});
|
|
||||||
resizeObserver.observe(container);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
contentChangeListener.dispose();
|
|
||||||
resizeObserver.disconnect();
|
|
||||||
editor?.dispose();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [monaco, editorId, function_call.name, function_call.arguments, onEdit]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack w="full" alignItems="flex-start">
|
|
||||||
<HStack w="full">
|
|
||||||
<Text fontWeight="bold" w={192}>
|
|
||||||
Name:
|
|
||||||
</Text>
|
|
||||||
<Input
|
|
||||||
value={function_call.name}
|
|
||||||
onChange={(e) => onEdit({ name: e.target.value, arguments: function_call.arguments })}
|
|
||||||
bgColor="white"
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
<Text fontWeight="bold" w={32}>
|
|
||||||
Arguments
|
|
||||||
</Text>
|
|
||||||
<VStack
|
|
||||||
borderRadius={4}
|
|
||||||
border="1px solid"
|
|
||||||
borderColor="gray.200"
|
|
||||||
w="full"
|
|
||||||
py={1}
|
|
||||||
bgColor="white"
|
|
||||||
>
|
|
||||||
<Box id={editorId} w="full" />
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FunctionCallEditor;
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { Box, Td, Tr, Thead, Th, Tooltip, HStack, Text, Checkbox } from "@chakra-ui/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import dayjs from "~/utils/dayjs";
|
|
||||||
import { type RouterOutputs } from "~/utils/api";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import { useIsClientRehydrated, useDatasetEntries } from "~/utils/hooks";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
type DatasetEntry = RouterOutputs["datasetEntries"]["list"]["entries"][0];
|
|
||||||
|
|
||||||
export const TableHeader = () => {
|
|
||||||
const matchingDatasetEntryIds = useDatasetEntries().data?.matchingEntryIds;
|
|
||||||
const selectedDatasetEntryIds = useAppStore((s) => s.selectedDatasetEntries.selectedIds);
|
|
||||||
const addSelectedIds = useAppStore((s) => s.selectedDatasetEntries.addSelectedIds);
|
|
||||||
const clearSelectedIds = useAppStore((s) => s.selectedDatasetEntries.clearSelectedIds);
|
|
||||||
const allSelected = useMemo(() => {
|
|
||||||
if (!matchingDatasetEntryIds || !matchingDatasetEntryIds.length) return false;
|
|
||||||
return matchingDatasetEntryIds.every((id) => selectedDatasetEntryIds.has(id));
|
|
||||||
}, [matchingDatasetEntryIds, selectedDatasetEntryIds]);
|
|
||||||
const isClientRehydrated = useIsClientRehydrated();
|
|
||||||
if (!isClientRehydrated) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th pr={0}>
|
|
||||||
<HStack minW={16}>
|
|
||||||
<Checkbox
|
|
||||||
isChecked={allSelected}
|
|
||||||
onChange={() => {
|
|
||||||
allSelected ? clearSelectedIds() : addSelectedIds(matchingDatasetEntryIds || []);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text>
|
|
||||||
({selectedDatasetEntryIds.size ? `${selectedDatasetEntryIds.size}/` : ""}
|
|
||||||
{matchingDatasetEntryIds?.length || 0})
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</Th>
|
|
||||||
<Th>Created At</Th>
|
|
||||||
<Th isNumeric>Input tokens</Th>
|
|
||||||
<Th isNumeric>Output tokens</Th>
|
|
||||||
<Th isNumeric>Type</Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TableRow = ({
|
|
||||||
datasetEntry,
|
|
||||||
onToggle,
|
|
||||||
showOptions,
|
|
||||||
}: {
|
|
||||||
datasetEntry: DatasetEntry;
|
|
||||||
onToggle: () => void;
|
|
||||||
showOptions?: boolean;
|
|
||||||
}) => {
|
|
||||||
const createdAt = dayjs(datasetEntry.createdAt).format("MMMM D h:mm A");
|
|
||||||
const fullTime = dayjs(datasetEntry.createdAt).toString();
|
|
||||||
|
|
||||||
const isChecked = useAppStore((s) => s.selectedDatasetEntries.selectedIds.has(datasetEntry.id));
|
|
||||||
const toggleChecked = useAppStore((s) => s.selectedDatasetEntries.toggleSelectedId);
|
|
||||||
|
|
||||||
const isClientRehydrated = useIsClientRehydrated();
|
|
||||||
if (!isClientRehydrated) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr
|
|
||||||
onClick={onToggle}
|
|
||||||
key={datasetEntry.id}
|
|
||||||
_hover={{ bgColor: "gray.50", cursor: "pointer" }}
|
|
||||||
fontSize="sm"
|
|
||||||
>
|
|
||||||
{showOptions && (
|
|
||||||
<Td>
|
|
||||||
<Checkbox isChecked={isChecked} onChange={() => toggleChecked(datasetEntry.id)} />
|
|
||||||
</Td>
|
|
||||||
)}
|
|
||||||
<Td>
|
|
||||||
<Tooltip label={fullTime} placement="top">
|
|
||||||
<Box whiteSpace="nowrap" minW="120px">
|
|
||||||
{createdAt}
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
</Td>
|
|
||||||
<Td isNumeric>{datasetEntry.inputTokens}</Td>
|
|
||||||
<Td isNumeric>{datasetEntry.outputTokens}</Td>
|
|
||||||
<Td isNumeric>{datasetEntry.type}</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EmptyTableRow = ({ filtersApplied = true }: { filtersApplied?: boolean }) => {
|
|
||||||
const visibleColumns = useAppStore((s) => s.columnVisibility.visibleColumns);
|
|
||||||
const filters = useAppStore((state) => state.logFilters.filters);
|
|
||||||
const { isLoading } = useDatasetEntries();
|
|
||||||
|
|
||||||
if (isLoading) return null;
|
|
||||||
|
|
||||||
if (filters.length && filtersApplied) {
|
|
||||||
return (
|
|
||||||
<Tr>
|
|
||||||
<Td w="full" colSpan={visibleColumns.size + 1}>
|
|
||||||
<Text color="gray.500" textAlign="center" w="full" p={4}>
|
|
||||||
No matching entries found. Try removing some filters.
|
|
||||||
</Text>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tr>
|
|
||||||
<Td w="full" colSpan={visibleColumns.size + 1}>
|
|
||||||
<Text color="gray.500" textAlign="center" w="full" p={4}>
|
|
||||||
This dataset has no entries. Add some logs in the{" "}
|
|
||||||
<Link href="/request-logs">
|
|
||||||
<Text as="span" color="blue.600">
|
|
||||||
Request Logs
|
|
||||||
</Text>
|
|
||||||
</Link>{" "}
|
|
||||||
tab.
|
|
||||||
</Text>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Button, HStack, Icon, Text } from "@chakra-ui/react";
|
|
||||||
import { useDataset } from "~/utils/hooks";
|
|
||||||
import { BsGearFill } from "react-icons/bs";
|
|
||||||
|
|
||||||
export const DatasetHeaderButtons = ({ openDrawer }: { openDrawer: () => void }) => {
|
|
||||||
const dataset = useDataset();
|
|
||||||
|
|
||||||
if (dataset.isLoading) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HStack spacing={0} mt={{ base: 2, md: 0 }}>
|
|
||||||
<Button variant={{ base: "solid", md: "ghost" }} onClick={openDrawer}>
|
|
||||||
<HStack>
|
|
||||||
<Icon as={BsGearFill} />
|
|
||||||
<Text>Configure</Text>
|
|
||||||
</HStack>
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
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,52 +0,0 @@
|
|||||||
import { Card, Table, Thead, Tr, Th, Tbody, Td, VStack, Icon, Text } from "@chakra-ui/react";
|
|
||||||
import { FaTable } from "react-icons/fa";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import dayjs from "~/utils/dayjs";
|
|
||||||
import { useDatasets } from "~/utils/hooks";
|
|
||||||
|
|
||||||
const DatasetsTable = ({}) => {
|
|
||||||
const { data } = useDatasets();
|
|
||||||
|
|
||||||
const datasets = data || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card width="100%" overflowX="auto">
|
|
||||||
{datasets.length ? (
|
|
||||||
<Table>
|
|
||||||
<Thead>
|
|
||||||
<Tr>
|
|
||||||
<Th>Name</Th>
|
|
||||||
<Th>Created At</Th>
|
|
||||||
<Th>Size</Th>
|
|
||||||
</Tr>
|
|
||||||
</Thead>
|
|
||||||
<Tbody>
|
|
||||||
{datasets.map((dataset) => {
|
|
||||||
return (
|
|
||||||
<Tr key={dataset.id}>
|
|
||||||
<Td>
|
|
||||||
<Link href={{ pathname: "/datasets/[id]", query: { id: dataset.id } }}>
|
|
||||||
<Text color="blue.600">{dataset.name}</Text>
|
|
||||||
</Link>
|
|
||||||
</Td>
|
|
||||||
<Td>{dayjs(dataset.createdAt).format("MMMM D h:mm A")}</Td>
|
|
||||||
<Td>{dataset._count.datasetEntries}</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
) : (
|
|
||||||
<VStack py={8}>
|
|
||||||
<Icon as={FaTable} boxSize={16} color="gray.300" />
|
|
||||||
<Text color="gray.400" fontSize="lg" fontWeight="bold">
|
|
||||||
No Datasets Found. Create your first dataset.
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DatasetsTable;
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
HStack,
|
|
||||||
VStack,
|
|
||||||
Icon,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
useDisclosure,
|
|
||||||
type UseDisclosureReturn,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { BsTrash } from "react-icons/bs";
|
|
||||||
|
|
||||||
import { useHandledAsyncCallback, useDataset } from "~/utils/hooks";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import ActionButton from "../ActionButton";
|
|
||||||
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
|
||||||
import pluralize from "pluralize";
|
|
||||||
|
|
||||||
const DeleteButton = () => {
|
|
||||||
const selectedIds = useAppStore((s) => s.selectedDatasetEntries.selectedIds);
|
|
||||||
|
|
||||||
const disclosure = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ActionButton
|
|
||||||
onClick={disclosure.onOpen}
|
|
||||||
label="Delete"
|
|
||||||
icon={BsTrash}
|
|
||||||
isDisabled={selectedIds.size === 0}
|
|
||||||
requireBeta
|
|
||||||
/>
|
|
||||||
<DeleteDatasetEntriesModal disclosure={disclosure} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeleteButton;
|
|
||||||
|
|
||||||
const DeleteDatasetEntriesModal = ({ disclosure }: { disclosure: UseDisclosureReturn }) => {
|
|
||||||
const dataset = useDataset().data;
|
|
||||||
const selectedIds = useAppStore((s) => s.selectedDatasetEntries.selectedIds);
|
|
||||||
const clearSelectedIds = useAppStore((s) => s.selectedDatasetEntries.clearSelectedIds);
|
|
||||||
|
|
||||||
const deleteRowsMutation = api.datasetEntries.delete.useMutation();
|
|
||||||
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const [deleteRows, deletionInProgress] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!dataset?.id || !selectedIds.size) return;
|
|
||||||
|
|
||||||
// divide selectedIds into chunks of 15000 to reduce request size
|
|
||||||
const chunkSize = 15000;
|
|
||||||
const idsArray = Array.from(selectedIds);
|
|
||||||
for (let i = 0; i < idsArray.length; i += chunkSize) {
|
|
||||||
const response = await deleteRowsMutation.mutateAsync({
|
|
||||||
ids: idsArray.slice(i, i + chunkSize),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (maybeReportError(response)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await utils.datasetEntries.list.invalidate();
|
|
||||||
disclosure.onClose();
|
|
||||||
clearSelectedIds();
|
|
||||||
}, [deleteRowsMutation, dataset, selectedIds, utils]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal size={{ base: "xl", md: "2xl" }} {...disclosure}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent w={1200}>
|
|
||||||
<ModalHeader>
|
|
||||||
<HStack>
|
|
||||||
<Icon as={BsTrash} />
|
|
||||||
<Text>Delete Logs</Text>
|
|
||||||
</HStack>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody maxW="unset">
|
|
||||||
<VStack w="full" spacing={8} pt={4} alignItems="flex-start">
|
|
||||||
<Text>
|
|
||||||
Are you sure you want to delete the <b>{selectedIds.size}</b>{" "}
|
|
||||||
{pluralize("row", selectedIds.size)} rows you've selected?
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<HStack>
|
|
||||||
<Button colorScheme="gray" onClick={disclosure.onClose} minW={24}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button colorScheme="red" onClick={deleteRows} isLoading={deletionInProgress} minW={24}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
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 { AiOutlineDownload } from "react-icons/ai";
|
|
||||||
|
|
||||||
import { useHandledAsyncCallback, useDataset } from "~/utils/hooks";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import ActionButton from "../ActionButton";
|
|
||||||
import { FiChevronUp, FiChevronDown } from "react-icons/fi";
|
|
||||||
import InfoCircle from "../InfoCircle";
|
|
||||||
|
|
||||||
const ExportButton = () => {
|
|
||||||
const selectedIds = useAppStore((s) => s.selectedDatasetEntries.selectedIds);
|
|
||||||
|
|
||||||
const disclosure = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ActionButton
|
|
||||||
onClick={disclosure.onOpen}
|
|
||||||
label="Download"
|
|
||||||
icon={AiOutlineDownload}
|
|
||||||
isDisabled={selectedIds.size === 0}
|
|
||||||
requireBeta
|
|
||||||
/>
|
|
||||||
<ExportDatasetEntriesModal disclosure={disclosure} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExportButton;
|
|
||||||
|
|
||||||
const ExportDatasetEntriesModal = ({ disclosure }: { disclosure: UseDisclosureReturn }) => {
|
|
||||||
const dataset = useDataset().data;
|
|
||||||
const selectedIds = useAppStore((s) => s.selectedDatasetEntries.selectedIds);
|
|
||||||
const clearSelectedIds = useAppStore((s) => s.selectedDatasetEntries.clearSelectedIds);
|
|
||||||
|
|
||||||
const [testingSplit, setTestingSplit] = useState(10);
|
|
||||||
const [removeDuplicates, setRemoveDuplicates] = useState(false);
|
|
||||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (disclosure.isOpen) {
|
|
||||||
setTestingSplit(10);
|
|
||||||
setRemoveDuplicates(false);
|
|
||||||
}
|
|
||||||
}, [disclosure.isOpen]);
|
|
||||||
|
|
||||||
const exportDataMutation = api.datasetEntries.export.useMutation();
|
|
||||||
|
|
||||||
const [exportData, exportInProgress] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!dataset?.id || !selectedIds.size || !testingSplit) return;
|
|
||||||
const response = await exportDataMutation.mutateAsync({
|
|
||||||
datasetId: dataset.id,
|
|
||||||
datasetEntryIds: Array.from(selectedIds),
|
|
||||||
testingSplit,
|
|
||||||
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();
|
|
||||||
clearSelectedIds();
|
|
||||||
}, [exportDataMutation, dataset, selectedIds, testingSplit, removeDuplicates]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal size={{ base: "xl", md: "2xl" }} {...disclosure}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent w={1200}>
|
|
||||||
<ModalHeader>
|
|
||||||
<HStack>
|
|
||||||
<Icon as={AiOutlineDownload} />
|
|
||||||
<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>{selectedIds.size}</b> rows you have selected in the OpenAI
|
|
||||||
training format.
|
|
||||||
</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">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={exportData} isLoading={exportInProgress} minW={24}>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { RiFlaskLine } from "react-icons/ri";
|
|
||||||
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import ActionButton from "../ActionButton";
|
|
||||||
|
|
||||||
const ExperimentButton = () => {
|
|
||||||
const selectedIds = useAppStore((s) => s.selectedDatasetEntries.selectedIds);
|
|
||||||
return (
|
|
||||||
<ActionButton
|
|
||||||
onClick={() => {
|
|
||||||
console.log("experimenting with these ids", selectedIds);
|
|
||||||
}}
|
|
||||||
label="Experiment"
|
|
||||||
icon={RiFlaskLine}
|
|
||||||
isDisabled={selectedIds.size === 0}
|
|
||||||
requireBeta
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExperimentButton;
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import { VStack, HStack, Button, Text, Progress, IconButton, Portal } from "@chakra-ui/react";
|
|
||||||
import { BsX } from "react-icons/bs";
|
|
||||||
|
|
||||||
import { type RouterOutputs, api } from "~/utils/api";
|
|
||||||
import { useDataset, useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import { formatFileSize } from "~/utils/utils";
|
|
||||||
|
|
||||||
type FileUpload = RouterOutputs["datasets"]["listFileUploads"][0];
|
|
||||||
|
|
||||||
const FileUploadsCard = () => {
|
|
||||||
const dataset = useDataset();
|
|
||||||
const [fileUploadsRefetchInterval, setFileUploadsRefetchInterval] = useState<number>(500);
|
|
||||||
const fileUploads = api.datasets.listFileUploads.useQuery(
|
|
||||||
{ datasetId: dataset.data?.id as string },
|
|
||||||
{ enabled: !!dataset.data?.id, refetchInterval: fileUploadsRefetchInterval },
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
|
||||||
if (fileUploads?.data?.some((fu) => fu.status !== "COMPLETE" && fu.status !== "ERROR")) {
|
|
||||||
setFileUploadsRefetchInterval(500);
|
|
||||||
} else {
|
|
||||||
setFileUploadsRefetchInterval(15000);
|
|
||||||
}
|
|
||||||
}, [fileUploads]);
|
|
||||||
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const hideFileUploadsMutation = api.datasets.hideFileUploads.useMutation();
|
|
||||||
const [hideAllFileUploads] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!fileUploads.data?.length) return;
|
|
||||||
await hideFileUploadsMutation.mutateAsync({
|
|
||||||
fileUploadIds: fileUploads.data.map((upload) => upload.id),
|
|
||||||
});
|
|
||||||
await utils.datasets.listFileUploads.invalidate();
|
|
||||||
}, [hideFileUploadsMutation, fileUploads.data, utils]);
|
|
||||||
|
|
||||||
if (!fileUploads.data?.length) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Portal>
|
|
||||||
<VStack
|
|
||||||
w={72}
|
|
||||||
borderRadius={8}
|
|
||||||
position="fixed"
|
|
||||||
bottom={8}
|
|
||||||
right={8}
|
|
||||||
overflow="hidden"
|
|
||||||
borderWidth={1}
|
|
||||||
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
|
|
||||||
minW={0}
|
|
||||||
bgColor="white"
|
|
||||||
>
|
|
||||||
<HStack p={4} w="full" bgColor="gray.200" justifyContent="space-between">
|
|
||||||
<Text fontWeight="bold">Uploads</Text>
|
|
||||||
<IconButton
|
|
||||||
aria-label="Close uploads"
|
|
||||||
as={BsX}
|
|
||||||
boxSize={6}
|
|
||||||
minW={0}
|
|
||||||
variant="ghost"
|
|
||||||
onClick={hideAllFileUploads}
|
|
||||||
cursor="pointer"
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
{fileUploads?.data?.map((upload) => <FileUploadRow key={upload.id} fileUpload={upload} />)}
|
|
||||||
</VStack>
|
|
||||||
</Portal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileUploadsCard;
|
|
||||||
|
|
||||||
const FileUploadRow = ({ fileUpload }: { fileUpload: FileUpload }) => {
|
|
||||||
const { id, fileName, fileSize, progress, status, errorMessage } = fileUpload;
|
|
||||||
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const hideFileUploadsMutation = api.datasets.hideFileUploads.useMutation();
|
|
||||||
const [hideFileUpload, hidingInProgress] = useHandledAsyncCallback(async () => {
|
|
||||||
await hideFileUploadsMutation.mutateAsync({ fileUploadIds: [id] });
|
|
||||||
}, [id, hideFileUploadsMutation, utils]);
|
|
||||||
|
|
||||||
const [refreshDatasetEntries] = useHandledAsyncCallback(async () => {
|
|
||||||
await hideFileUploadsMutation.mutateAsync({ fileUploadIds: [id] });
|
|
||||||
await utils.datasets.listFileUploads.invalidate();
|
|
||||||
await utils.datasetEntries.list.invalidate();
|
|
||||||
}, [id, hideFileUploadsMutation, utils]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VStack w="full" alignItems="flex-start" p={4} borderBottomWidth={1}>
|
|
||||||
<HStack w="full" justifyContent="space-between" alignItems="flex-start">
|
|
||||||
<VStack alignItems="flex-start" spacing={0}>
|
|
||||||
<Text fontWeight="bold">{fileName}</Text>
|
|
||||||
<Text fontSize="xs">({formatFileSize(fileSize, 2)})</Text>
|
|
||||||
</VStack>
|
|
||||||
|
|
||||||
<HStack spacing={0}>
|
|
||||||
{status === "COMPLETE" ? (
|
|
||||||
<Button variant="ghost" onClick={refreshDatasetEntries} color="orange.400" size="xs">
|
|
||||||
Refresh Table
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<IconButton
|
|
||||||
aria-label="Hide file upload"
|
|
||||||
as={BsX}
|
|
||||||
boxSize={6}
|
|
||||||
minW={0}
|
|
||||||
variant="ghost"
|
|
||||||
isLoading={hidingInProgress}
|
|
||||||
onClick={hideFileUpload}
|
|
||||||
cursor="pointer"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</HStack>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
{errorMessage ? (
|
|
||||||
<Text alignSelf="center" pt={2}>
|
|
||||||
{errorMessage}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Text alignSelf="center" fontSize="xs">
|
|
||||||
{getStatusText(status)}
|
|
||||||
</Text>
|
|
||||||
<Progress w="full" value={progress} borderRadius={2} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatusText = (status: FileUpload["status"]) => {
|
|
||||||
switch (status) {
|
|
||||||
case "PENDING":
|
|
||||||
return "Pending";
|
|
||||||
case "DOWNLOADING":
|
|
||||||
return "Downloading to Server";
|
|
||||||
case "PROCESSING":
|
|
||||||
return "Processing";
|
|
||||||
case "SAVING":
|
|
||||||
return "Saving";
|
|
||||||
case "COMPLETE":
|
|
||||||
return "Complete";
|
|
||||||
case "ERROR":
|
|
||||||
return "Error";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
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 { AiTwotoneThunderbolt } from "react-icons/ai";
|
|
||||||
import humanId from "human-id";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import { useDataset, useDatasetEntries, useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
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 datasetEntries = useDatasetEntries().data;
|
|
||||||
|
|
||||||
const numEntries = datasetEntries?.matchingEntryIds.length || 0;
|
|
||||||
|
|
||||||
const disclosure = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ActionButton
|
|
||||||
onClick={disclosure.onOpen}
|
|
||||||
label="Fine Tune"
|
|
||||||
icon={AiTwotoneThunderbolt}
|
|
||||||
isDisabled={numEntries === 0}
|
|
||||||
requireBeta
|
|
||||||
/>
|
|
||||||
<FineTuneModal disclosure={disclosure} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FineTuneButton;
|
|
||||||
|
|
||||||
const FineTuneModal = ({ disclosure }: { disclosure: UseDisclosureReturn }) => {
|
|
||||||
const dataset = useDataset().data;
|
|
||||||
const datasetEntries = useDatasetEntries().data;
|
|
||||||
|
|
||||||
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 (!modelSlug || !selectedBaseModel || !dataset) return;
|
|
||||||
await createFineTuneMutation.mutateAsync({
|
|
||||||
slug: modelSlug,
|
|
||||||
baseModel: selectedBaseModel,
|
|
||||||
datasetId: dataset.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.fineTunes.list.invalidate();
|
|
||||||
await router.push({ pathname: "/fine-tunes" });
|
|
||||||
disclosure.onClose();
|
|
||||||
}, [createFineTuneMutation, modelSlug, selectedBaseModel]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal size={{ base: "xl", md: "2xl" }} {...disclosure}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent w={1200}>
|
|
||||||
<ModalHeader>
|
|
||||||
<HStack>
|
|
||||||
<Icon as={AiTwotoneThunderbolt} />
|
|
||||||
<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 <b>{datasetEntries?.trainingCount}</b> and test on{" "}
|
|
||||||
<b>{datasetEntries?.testingCount}</b> entries in this dataset.
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
13
app/src/components/datasets/TableRow.tsx
Normal file
13
app/src/components/datasets/TableRow.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
HStack,
|
|
||||||
VStack,
|
|
||||||
Icon,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
Box,
|
|
||||||
useDisclosure,
|
|
||||||
type UseDisclosureReturn,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import pluralize from "pluralize";
|
|
||||||
import { AiOutlineCloudUpload, AiOutlineFile } from "react-icons/ai";
|
|
||||||
|
|
||||||
import { useDataset, useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import ActionButton from "../ActionButton";
|
|
||||||
import { validateTrainingRows, type TrainingRow, parseJSONL } from "./validateTrainingRows";
|
|
||||||
import { uploadDatasetEntryFile } from "~/utils/azure/website";
|
|
||||||
import { formatFileSize } from "~/utils/utils";
|
|
||||||
|
|
||||||
const UploadDataButton = () => {
|
|
||||||
const disclosure = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ActionButton
|
|
||||||
onClick={disclosure.onOpen}
|
|
||||||
label="Upload Data"
|
|
||||||
icon={AiOutlineCloudUpload}
|
|
||||||
iconBoxSize={4}
|
|
||||||
requireBeta
|
|
||||||
/>
|
|
||||||
<UploadDataModal disclosure={disclosure} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UploadDataButton;
|
|
||||||
|
|
||||||
const UploadDataModal = ({ disclosure }: { disclosure: UseDisclosureReturn }) => {
|
|
||||||
const dataset = useDataset().data;
|
|
||||||
|
|
||||||
const [validationError, setValidationError] = useState<string | null>(null);
|
|
||||||
const [trainingRows, setTrainingRows] = useState<TrainingRow[] | null>(null);
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleFileDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const files = e.dataTransfer.files;
|
|
||||||
if (files.length > 0) {
|
|
||||||
processFile(files[0] as File);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const files = e.target.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
processFile(files[0] as File);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const processFile = (file: File) => {
|
|
||||||
setFile(file);
|
|
||||||
|
|
||||||
// skip reading if file is larger than 10MB
|
|
||||||
if (file.size > 10000000) {
|
|
||||||
setTrainingRows(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e: ProgressEvent<FileReader>) => {
|
|
||||||
const content = e.target?.result as string;
|
|
||||||
// Process the content, e.g., set to state
|
|
||||||
let parsedJSONL;
|
|
||||||
try {
|
|
||||||
parsedJSONL = parseJSONL(content) as TrainingRow[];
|
|
||||||
const validationError = validateTrainingRows(parsedJSONL);
|
|
||||||
if (validationError) {
|
|
||||||
setValidationError(validationError);
|
|
||||||
setTrainingRows(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTrainingRows(parsedJSONL);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (e: any) {
|
|
||||||
setValidationError("Unable to parse JSONL file: " + (e.message as string));
|
|
||||||
setTrainingRows(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetState = useCallback(() => {
|
|
||||||
setValidationError(null);
|
|
||||||
setTrainingRows(null);
|
|
||||||
setFile(null);
|
|
||||||
}, [setValidationError, setTrainingRows, setFile]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (disclosure.isOpen) {
|
|
||||||
resetState();
|
|
||||||
}
|
|
||||||
}, [disclosure.isOpen, resetState]);
|
|
||||||
|
|
||||||
const triggerFileDownloadMutation = api.datasets.triggerFileDownload.useMutation();
|
|
||||||
|
|
||||||
const utils = api.useContext();
|
|
||||||
|
|
||||||
const [sendJSONL, sendingInProgress] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!dataset || !file) return;
|
|
||||||
|
|
||||||
const blobName = await uploadDatasetEntryFile(file);
|
|
||||||
|
|
||||||
await triggerFileDownloadMutation.mutateAsync({
|
|
||||||
datasetId: dataset.id,
|
|
||||||
blobName,
|
|
||||||
fileName: file.name,
|
|
||||||
fileSize: file.size,
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.datasets.listFileUploads.invalidate();
|
|
||||||
|
|
||||||
disclosure.onClose();
|
|
||||||
}, [dataset, trainingRows, triggerFileDownloadMutation, file, utils]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal size={{ base: "xl", md: "2xl" }} {...disclosure}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent w={1200}>
|
|
||||||
<ModalHeader>
|
|
||||||
<HStack>
|
|
||||||
<Text>Upload Training Logs</Text>
|
|
||||||
</HStack>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody maxW="unset" p={8}>
|
|
||||||
<Box w="full" aspectRatio={1.5}>
|
|
||||||
{validationError && (
|
|
||||||
<VStack w="full" h="full" justifyContent="center" spacing={8}>
|
|
||||||
<Icon as={AiOutlineFile} boxSize={24} color="gray.300" />
|
|
||||||
<VStack w="full">
|
|
||||||
<Text fontSize={32} color="gray.500" fontWeight="bold">
|
|
||||||
Error
|
|
||||||
</Text>
|
|
||||||
<Text color="gray.500">{validationError}</Text>
|
|
||||||
</VStack>
|
|
||||||
<Text
|
|
||||||
as="span"
|
|
||||||
textDecor="underline"
|
|
||||||
color="gray.500"
|
|
||||||
_hover={{ color: "orange.400" }}
|
|
||||||
cursor="pointer"
|
|
||||||
onClick={resetState}
|
|
||||||
>
|
|
||||||
Try again
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
{!validationError && !file && (
|
|
||||||
<VStack
|
|
||||||
w="full"
|
|
||||||
h="full"
|
|
||||||
stroke="gray.300"
|
|
||||||
justifyContent="center"
|
|
||||||
borderRadius={8}
|
|
||||||
sx={{
|
|
||||||
"background-image": `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect x='2%25' y='2%25' width='96%25' height='96%25' fill='none' stroke='%23eee' stroke-width='4' stroke-dasharray='6%2c 14' stroke-dashoffset='0' stroke-linecap='square' rx='8' ry='8'/%3e%3c/svg%3e")`,
|
|
||||||
}}
|
|
||||||
onDragOver={(e) => e.preventDefault()}
|
|
||||||
onDrop={handleFileDrop}
|
|
||||||
>
|
|
||||||
<JsonFileIcon />
|
|
||||||
<Icon as={AiOutlineCloudUpload} boxSize={24} color="gray.300" />
|
|
||||||
|
|
||||||
<Text fontSize={32} color="gray.500" fontWeight="bold">
|
|
||||||
Drag & Drop
|
|
||||||
</Text>
|
|
||||||
<Text color="gray.500">
|
|
||||||
your .jsonl file here, or{" "}
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
accept=".jsonl"
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
as="span"
|
|
||||||
textDecor="underline"
|
|
||||||
_hover={{ color: "orange.400" }}
|
|
||||||
cursor="pointer"
|
|
||||||
onClick={() => fileInputRef.current?.click()}
|
|
||||||
>
|
|
||||||
browse
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
{!validationError && file && (
|
|
||||||
<VStack w="full" h="full" justifyContent="center" spacing={8}>
|
|
||||||
<JsonFileIcon />
|
|
||||||
<VStack w="full">
|
|
||||||
{trainingRows ? (
|
|
||||||
<>
|
|
||||||
<Text fontSize={32} color="gray.500" fontWeight="bold">
|
|
||||||
Success
|
|
||||||
</Text>
|
|
||||||
<Text color="gray.500">
|
|
||||||
We'll upload <b>{trainingRows.length}</b>{" "}
|
|
||||||
{pluralize("row", trainingRows.length)} into <b>{dataset?.name}</b>.{" "}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Text fontSize={32} color="gray.500" fontWeight="bold">
|
|
||||||
{file.name}
|
|
||||||
</Text>
|
|
||||||
<Text color="gray.500">{formatFileSize(file.size)}</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
<Text
|
|
||||||
as="span"
|
|
||||||
textDecor="underline"
|
|
||||||
color="gray.500"
|
|
||||||
_hover={{ color: "orange.400" }}
|
|
||||||
cursor="pointer"
|
|
||||||
onClick={resetState}
|
|
||||||
>
|
|
||||||
Change file
|
|
||||||
</Text>
|
|
||||||
</VStack>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<HStack>
|
|
||||||
<Button colorScheme="gray" onClick={disclosure.onClose} minW={24}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorScheme="orange"
|
|
||||||
onClick={sendJSONL}
|
|
||||||
isLoading={sendingInProgress}
|
|
||||||
minW={24}
|
|
||||||
isDisabled={!file || !!validationError}
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const JsonFileIcon = () => (
|
|
||||||
<Box position="relative" display="flex" alignItems="center" justifyContent="center">
|
|
||||||
<Icon as={AiOutlineFile} boxSize={24} color="gray.300" />
|
|
||||||
<Text position="absolute" color="orange.400" fontWeight="bold" fontSize={12} pt={4}>
|
|
||||||
JSONL
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { type CreateChatCompletionRequestMessage } from "openai/resources/chat";
|
|
||||||
|
|
||||||
export type TrainingRow = {
|
|
||||||
input: CreateChatCompletionRequestMessage[];
|
|
||||||
output?: CreateChatCompletionRequestMessage;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseJSONL = (jsonlString: string): unknown[] => {
|
|
||||||
const lines = jsonlString.trim().split("\n");
|
|
||||||
|
|
||||||
let lineNumber = 0;
|
|
||||||
const parsedLines = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (const line of lines) {
|
|
||||||
lineNumber++;
|
|
||||||
parsedLines.push(JSON.parse(line));
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (e: any) {
|
|
||||||
throw new Error(`Error parsing line ${lineNumber}: ${e.message as string}`);
|
|
||||||
}
|
|
||||||
return parsedLines;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateTrainingRows = (rows: unknown): string | null => {
|
|
||||||
if (!Array.isArray(rows)) return "training data is not an array";
|
|
||||||
for (let i = 0; i < rows.length; i++) {
|
|
||||||
const row = rows[i] as TrainingRow;
|
|
||||||
let errorMessage: string | null = null;
|
|
||||||
try {
|
|
||||||
errorMessage = validateTrainingRow(row);
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} catch (error: any) {
|
|
||||||
errorMessage = error.message;
|
|
||||||
}
|
|
||||||
if (errorMessage) return `row ${i + 1}: ${errorMessage}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateTrainingRow = (row: TrainingRow): string | null => {
|
|
||||||
if (!row) return "empty row";
|
|
||||||
if (!row.input) return "missing input";
|
|
||||||
|
|
||||||
// Validate input
|
|
||||||
if (!Array.isArray(row.input)) return "input is not an array";
|
|
||||||
if ((row.input as unknown[]).some((x) => typeof x !== "object"))
|
|
||||||
return "input contains invalid item";
|
|
||||||
if (row.input.some((x) => !x)) return "input contains empty item";
|
|
||||||
if (row.input.some((x) => !x.content && !x.function_call))
|
|
||||||
return "input contains item with no content or function_call";
|
|
||||||
if (row.input.some((x) => x.function_call && !x.function_call.arguments))
|
|
||||||
return "input contains item with function_call but no arguments";
|
|
||||||
if (row.input.some((x) => x.function_call && !x.function_call.name))
|
|
||||||
return "input contains item with function_call but no name";
|
|
||||||
|
|
||||||
// Validate output
|
|
||||||
if (row.output) {
|
|
||||||
if (typeof row.output !== "object") return "output is not an object";
|
|
||||||
if (!row.output.content && !row.output.function_call)
|
|
||||||
return "output contains no content or function_call";
|
|
||||||
if (row.output.function_call && !row.output.function_call.arguments)
|
|
||||||
return "output contains function_call but no arguments";
|
|
||||||
if (row.output.function_call && !row.output.function_call.name)
|
|
||||||
return "output contains function_call but no name";
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { type MouseEvent, useState } from "react";
|
|
||||||
import {
|
import {
|
||||||
HStack,
|
HStack,
|
||||||
Icon,
|
Icon,
|
||||||
@@ -9,29 +8,27 @@ import {
|
|||||||
AspectRatio,
|
AspectRatio,
|
||||||
SkeletonText,
|
SkeletonText,
|
||||||
Card,
|
Card,
|
||||||
useDisclosure,
|
|
||||||
Box,
|
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuList,
|
|
||||||
MenuItem,
|
|
||||||
IconButton,
|
|
||||||
useToast,
|
|
||||||
} 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 Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { BsPlusSquare, BsThreeDotsVertical, BsLink45Deg, BsTrash } from "react-icons/bs";
|
import { BsPlusSquare } from "react-icons/bs";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
import { formatTimePast } from "~/utils/dayjs";
|
|
||||||
import { type 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";
|
||||||
import DeleteExperimentDialog from "./DeleteExperimentDialog";
|
|
||||||
|
|
||||||
export const ExperimentCard = ({ exp }: { exp: RouterOutputs["experiments"]["list"][0] }) => {
|
type ExperimentData = {
|
||||||
const [isMenuHovered, setIsMenuHovered] = useState(false);
|
testScenarioCount: number;
|
||||||
|
promptVariantCount: number;
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
sortIndex: number;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExperimentCard = ({ exp }: { exp: ExperimentData }) => {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
w="full"
|
w="full"
|
||||||
@@ -40,7 +37,7 @@ export const ExperimentCard = ({ exp }: { exp: RouterOutputs["experiments"]["lis
|
|||||||
p={4}
|
p={4}
|
||||||
bg="white"
|
bg="white"
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
_hover={{ bg: isMenuHovered ? undefined : "gray.100" }}
|
_hover={{ bg: "gray.100" }}
|
||||||
transition="background 0.2s"
|
transition="background 0.2s"
|
||||||
aspectRatio={1.2}
|
aspectRatio={1.2}
|
||||||
>
|
>
|
||||||
@@ -48,21 +45,13 @@ export const ExperimentCard = ({ exp }: { exp: RouterOutputs["experiments"]["lis
|
|||||||
as={Link}
|
as={Link}
|
||||||
w="full"
|
w="full"
|
||||||
h="full"
|
h="full"
|
||||||
href={{ pathname: "/experiments/[experimentSlug]", query: { experimentSlug: exp.slug } }}
|
href={{ pathname: "/experiments/[id]", query: { id: exp.id } }}
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
>
|
>
|
||||||
<HStack w="full" justify="space-between" spacing={0}>
|
<HStack w="full" color="gray.700" justify="center">
|
||||||
<Box w={6} />
|
|
||||||
<HStack color="gray.700" justify="center">
|
|
||||||
<Icon as={RiFlaskLine} boxSize={4} />
|
<Icon as={RiFlaskLine} boxSize={4} />
|
||||||
<Text fontWeight="bold">{exp.label}</Text>
|
<Text fontWeight="bold">{exp.label}</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<CardMenu
|
|
||||||
experimentId={exp.id}
|
|
||||||
experimentSlug={exp.slug}
|
|
||||||
setIsMenuHovered={setIsMenuHovered}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
<HStack h="full" spacing={4} flex={1} align="center">
|
<HStack h="full" spacing={4} flex={1} align="center">
|
||||||
<CountLabel label="Variants" count={exp.promptVariantCount} />
|
<CountLabel label="Variants" count={exp.promptVariantCount} />
|
||||||
<Divider h={12} orientation="vertical" />
|
<Divider h={12} orientation="vertical" />
|
||||||
@@ -78,75 +67,6 @@ export const ExperimentCard = ({ exp }: { exp: RouterOutputs["experiments"]["lis
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CardMenu = ({
|
|
||||||
experimentId,
|
|
||||||
experimentSlug,
|
|
||||||
setIsMenuHovered,
|
|
||||||
}: {
|
|
||||||
experimentId: string;
|
|
||||||
experimentSlug: string;
|
|
||||||
setIsMenuHovered: (isHovered: boolean) => void;
|
|
||||||
}) => {
|
|
||||||
const deleteDisclosure = useDisclosure();
|
|
||||||
const menuDisclosure = useDisclosure();
|
|
||||||
const toast = useToast();
|
|
||||||
const [copyShareLink] = useHandledAsyncCallback(
|
|
||||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
const shareLink = `${window.location.origin}/experiments/${experimentSlug}`;
|
|
||||||
await navigator.clipboard.writeText(shareLink);
|
|
||||||
toast({
|
|
||||||
title: "Share link copied to clipboard",
|
|
||||||
status: "success",
|
|
||||||
duration: 2000,
|
|
||||||
isClosable: true,
|
|
||||||
});
|
|
||||||
menuDisclosure.onClose();
|
|
||||||
},
|
|
||||||
[toast, menuDisclosure.onClose, experimentSlug],
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Menu isLazy {...menuDisclosure}>
|
|
||||||
<MenuButton
|
|
||||||
as={IconButton}
|
|
||||||
aria-label="Options"
|
|
||||||
icon={<BsThreeDotsVertical />}
|
|
||||||
variant="ghost"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
menuDisclosure.onOpen();
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setIsMenuHovered(true)}
|
|
||||||
onMouseLeave={() => setIsMenuHovered(false)}
|
|
||||||
boxSize={6}
|
|
||||||
minW={0}
|
|
||||||
/>
|
|
||||||
<MenuList>
|
|
||||||
<MenuItem icon={<Icon as={BsLink45Deg} boxSize={5} />} onClick={copyShareLink}>
|
|
||||||
Copy Link
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
icon={<Icon as={BsTrash} boxSize={5} />}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
deleteDisclosure.onOpen();
|
|
||||||
}}
|
|
||||||
color="red.500"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
<DeleteExperimentDialog experimentId={experimentId} disclosure={deleteDisclosure} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const CountLabel = ({ label, count }: { label: string; count: number }) => {
|
const CountLabel = ({ label, count }: { label: string; count: number }) => {
|
||||||
return (
|
return (
|
||||||
<VStack alignItems="center" flex={1}>
|
<VStack alignItems="center" flex={1}>
|
||||||
@@ -169,8 +89,8 @@ export const NewExperimentCard = () => {
|
|||||||
projectId: selectedProjectId ?? "",
|
projectId: selectedProjectId ?? "",
|
||||||
});
|
});
|
||||||
await router.push({
|
await router.push({
|
||||||
pathname: "/experiments/[experimentSlug]",
|
pathname: "/experiments/[id]",
|
||||||
query: { experimentSlug: newExperiment.slug },
|
query: { id: newExperiment.id },
|
||||||
});
|
});
|
||||||
}, [createMutation, router, selectedProjectId]);
|
}, [createMutation, router, selectedProjectId]);
|
||||||
|
|
||||||
@@ -188,7 +108,9 @@ export const NewExperimentCard = () => {
|
|||||||
>
|
>
|
||||||
<VStack align="center" justify="center" w="full" h="full" p={4} 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 ml={2}>New Experiment</Text>
|
<Text display={{ base: "none", md: "block" }} ml={2}>
|
||||||
|
New Experiment
|
||||||
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,43 +1,36 @@
|
|||||||
import { useRef } from "react";
|
|
||||||
import {
|
import {
|
||||||
type UseDisclosureReturn,
|
Button,
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogOverlay,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogBody,
|
AlertDialogBody,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
Button,
|
AlertDialogHeader,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogOverlay,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useRef } from "react";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
||||||
|
|
||||||
import { useHandledAsyncCallback } from "~/utils/hooks";
|
export const DeleteDialog = ({ onClose }: { onClose: () => void }) => {
|
||||||
|
const experiment = useExperiment();
|
||||||
|
const deleteMutation = api.experiments.delete.useMutation();
|
||||||
|
const utils = api.useContext();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const DeleteExperimentDialog = ({
|
|
||||||
experimentId,
|
|
||||||
onDelete,
|
|
||||||
disclosure,
|
|
||||||
}: {
|
|
||||||
experimentId?: string;
|
|
||||||
onDelete?: () => void;
|
|
||||||
disclosure: UseDisclosureReturn;
|
|
||||||
}) => {
|
|
||||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
const mutation = api.experiments.delete.useMutation();
|
const [onDeleteConfirm] = useHandledAsyncCallback(async () => {
|
||||||
const utils = api.useContext();
|
if (!experiment.data?.id) return;
|
||||||
|
await deleteMutation.mutateAsync({ id: experiment.data.id });
|
||||||
const [onDeleteConfirm, deletionInProgress] = useHandledAsyncCallback(async () => {
|
|
||||||
if (!experimentId) return;
|
|
||||||
await mutation.mutateAsync({ id: experimentId });
|
|
||||||
await utils.experiments.list.invalidate();
|
await utils.experiments.list.invalidate();
|
||||||
onDelete?.();
|
await router.push({ pathname: "/experiments" });
|
||||||
|
onClose();
|
||||||
disclosure.onClose();
|
}, [deleteMutation, experiment.data?.id, router]);
|
||||||
}, [mutation, experimentId, disclosure.onClose]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog leastDestructiveRef={cancelRef} {...disclosure}>
|
<AlertDialog isOpen leastDestructiveRef={cancelRef} onClose={onClose}>
|
||||||
<AlertDialogOverlay>
|
<AlertDialogOverlay>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||||
@@ -50,15 +43,10 @@ const DeleteExperimentDialog = ({
|
|||||||
</AlertDialogBody>
|
</AlertDialogBody>
|
||||||
|
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<Button ref={cancelRef} onClick={disclosure.onClose}>
|
<Button ref={cancelRef} onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button colorScheme="red" onClick={onDeleteConfirm} ml={3}>
|
||||||
colorScheme="red"
|
|
||||||
isLoading={deletionInProgress}
|
|
||||||
onClick={onDeleteConfirm}
|
|
||||||
ml={3}
|
|
||||||
>
|
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
@@ -67,5 +55,3 @@ const DeleteExperimentDialog = ({
|
|||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DeleteExperimentDialog;
|
|
||||||
@@ -3,14 +3,17 @@ import { useOnForkButtonPressed } from "./useOnForkButtonPressed";
|
|||||||
import { useExperiment } from "~/utils/hooks";
|
import { useExperiment } from "~/utils/hooks";
|
||||||
import { BsGearFill } from "react-icons/bs";
|
import { BsGearFill } from "react-icons/bs";
|
||||||
import { TbGitFork } from "react-icons/tb";
|
import { TbGitFork } from "react-icons/tb";
|
||||||
|
import { useAppStore } from "~/state/store";
|
||||||
|
|
||||||
export const ExperimentHeaderButtons = ({ openDrawer }: { openDrawer: () => void }) => {
|
export const ExperimentHeaderButtons = () => {
|
||||||
const experiment = useExperiment();
|
const experiment = useExperiment();
|
||||||
|
|
||||||
const canModify = experiment.data?.access.canModify ?? false;
|
const canModify = experiment.data?.access.canModify ?? false;
|
||||||
|
|
||||||
const { onForkButtonPressed, isForking } = useOnForkButtonPressed();
|
const { onForkButtonPressed, isForking } = useOnForkButtonPressed();
|
||||||
|
|
||||||
|
const openDrawer = useAppStore((s) => s.openDrawer);
|
||||||
|
|
||||||
if (experiment.isLoading) return null;
|
if (experiment.isLoading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,14 +16,11 @@ 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 newExperiment = await forkMutation.mutateAsync({
|
const forkedExperimentId = await forkMutation.mutateAsync({
|
||||||
id: experiment.data.id,
|
id: experiment.data.id,
|
||||||
projectId: selectedProjectId,
|
projectId: selectedProjectId,
|
||||||
});
|
});
|
||||||
await router.push({
|
await router.push({ pathname: "/experiments/[id]", query: { id: forkedExperimentId } });
|
||||||
pathname: "/experiments/[experimentSlug]",
|
|
||||||
query: { experimentSlug: newExperiment.slug },
|
|
||||||
});
|
|
||||||
}, [forkMutation, experiment.data?.id, router]);
|
}, [forkMutation, experiment.data?.id, router]);
|
||||||
|
|
||||||
const onForkButtonPressed = useCallback(() => {
|
const onForkButtonPressed = useCallback(() => {
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Button, Icon, useDisclosure, Text } from "@chakra-ui/react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { BsTrash } from "react-icons/bs";
|
|
||||||
|
|
||||||
import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import DeleteExperimentDialog from "../DeleteExperimentDialog";
|
|
||||||
|
|
||||||
export const DeleteButton = ({ closeDrawer }: { closeDrawer: () => void }) => {
|
|
||||||
const experiment = useExperiment();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const disclosure = useDisclosure();
|
|
||||||
|
|
||||||
const [onDelete] = useHandledAsyncCallback(async () => {
|
|
||||||
await router.push({ pathname: "/experiments" });
|
|
||||||
closeDrawer();
|
|
||||||
}, [router, closeDrawer]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
colorScheme="red"
|
|
||||||
fontWeight="normal"
|
|
||||||
onClick={disclosure.onOpen}
|
|
||||||
>
|
|
||||||
<Icon as={BsTrash} boxSize={4} />
|
|
||||||
<Text ml={2}>Delete Experiment</Text>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<DeleteExperimentDialog
|
|
||||||
experimentId={experiment.data?.id}
|
|
||||||
onDelete={onDelete}
|
|
||||||
disclosure={disclosure}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -13,19 +13,14 @@ import {
|
|||||||
} 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 { useRouter } from "next/router";
|
|
||||||
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 { RiHome3Line, RiFlaskLine } from "react-icons/ri";
|
import { RiHome3Line, RiDatabase2Line, RiFlaskLine } from "react-icons/ri";
|
||||||
import { AiOutlineThunderbolt, AiOutlineDatabase } from "react-icons/ai";
|
|
||||||
import { FaReadme } from "react-icons/fa";
|
|
||||||
import { signIn, useSession } from "next-auth/react";
|
import { signIn, useSession } from "next-auth/react";
|
||||||
|
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" />;
|
||||||
|
|
||||||
@@ -76,11 +71,21 @@ const NavSidebar = () => {
|
|||||||
<ProjectMenu />
|
<ProjectMenu />
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<IconLink icon={RiHome3Line} label="Dashboard" href="/dashboard" />
|
{env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS && (
|
||||||
<IconLink icon={IoStatsChartOutline} label="Request Logs" href="/request-logs" />
|
<>
|
||||||
<IconLink icon={AiOutlineDatabase} label="Datasets" href="/datasets" beta />
|
<IconLink icon={RiHome3Line} label="Dashboard" href="/dashboard" beta />
|
||||||
<IconLink icon={AiOutlineThunderbolt} label="Fine Tunes" href="/fine-tunes" beta />
|
<IconLink
|
||||||
|
icon={IoStatsChartOutline}
|
||||||
|
label="Request Logs"
|
||||||
|
href="/request-logs"
|
||||||
|
beta
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
<IconLink icon={RiFlaskLine} label="Experiments" href="/experiments" />
|
||||||
|
{env.NEXT_PUBLIC_SHOW_DATA && (
|
||||||
|
<IconLink icon={RiDatabase2Line} label="Data" href="/data" />
|
||||||
|
)}
|
||||||
<VStack w="full" alignItems="flex-start" spacing={0} pt={8}>
|
<VStack w="full" alignItems="flex-start" spacing={0} pt={8}>
|
||||||
<Text
|
<Text
|
||||||
pl={2}
|
pl={2}
|
||||||
@@ -100,7 +105,7 @@ const NavSidebar = () => {
|
|||||||
<NavSidebarOption>
|
<NavSidebarOption>
|
||||||
<HStack
|
<HStack
|
||||||
w="full"
|
w="full"
|
||||||
p={{ base: 2, md: 4 }}
|
p={4}
|
||||||
as={ChakraLink}
|
as={ChakraLink}
|
||||||
justifyContent="start"
|
justifyContent="start"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -115,22 +120,7 @@ const NavSidebar = () => {
|
|||||||
</NavSidebarOption>
|
</NavSidebarOption>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
<HStack
|
|
||||||
w="full"
|
|
||||||
px={{ base: 3, md: 4 }}
|
|
||||||
py={{ base: 0, md: 1 }}
|
|
||||||
as={ChakraLink}
|
|
||||||
justifyContent="start"
|
|
||||||
href="https://docs.openpipe.ai"
|
|
||||||
target="_blank"
|
|
||||||
color="gray.500"
|
|
||||||
spacing={1}
|
|
||||||
>
|
|
||||||
<Icon as={FaReadme} boxSize={4} mr={2} />
|
|
||||||
<Text fontWeight="bold" fontSize="sm" display={{ base: "none", md: "flex" }}>
|
|
||||||
Open Documentation
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<VStack spacing={0} align="center">
|
<VStack spacing={0} align="center">
|
||||||
<ChakraLink
|
<ChakraLink
|
||||||
@@ -151,15 +141,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
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setHeight = () => {
|
const setHeight = () => {
|
||||||
@@ -187,11 +174,7 @@ 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">
|
<Flex h={vh} w="100vw">
|
||||||
<Head>
|
<Head>
|
||||||
<title>{title ? `${title} | OpenPipe` : "OpenPipe"}</title>
|
<title>{title ? `${title} | OpenPipe` : "OpenPipe"}</title>
|
||||||
@@ -201,7 +184,5 @@ export default function AppShell({
|
|||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
<BetaModal isOpen={!!requireBeta && flagsLoaded && !flags.betaAccess} onClose={router.back} />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ import {
|
|||||||
Link as ChakraLink,
|
Link as ChakraLink,
|
||||||
Image,
|
Image,
|
||||||
Box,
|
Box,
|
||||||
Portal,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { BsPlus, BsPersonCircle } from "react-icons/bs";
|
import { BsPlus, BsPersonCircle } from "react-icons/bs";
|
||||||
import { type Project } from "@prisma/client";
|
import { type Project } from "@prisma/client";
|
||||||
@@ -57,7 +56,6 @@ export default function ProjectMenu() {
|
|||||||
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" });
|
||||||
popover.onClose();
|
|
||||||
}, [createMutation, router]);
|
}, [createMutation, router]);
|
||||||
|
|
||||||
const user = useSession().data;
|
const user = useSession().data;
|
||||||
@@ -69,13 +67,7 @@ export default function ProjectMenu() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack
|
<VStack w="full" alignItems="flex-start" spacing={0} py={1}>
|
||||||
w="full"
|
|
||||||
alignItems="flex-start"
|
|
||||||
spacing={0}
|
|
||||||
py={1}
|
|
||||||
zIndex={popover.isOpen ? "dropdown" : undefined}
|
|
||||||
>
|
|
||||||
<Popover
|
<Popover
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
isOpen={popover.isOpen}
|
isOpen={popover.isOpen}
|
||||||
@@ -111,11 +103,10 @@ export default function ProjectMenu() {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</NavSidebarOption>
|
</NavSidebarOption>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<Portal>
|
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
_focusVisible={{ outline: "unset" }}
|
_focusVisible={{ outline: "unset" }}
|
||||||
w={220}
|
ml={-1}
|
||||||
ml={{ base: 2, md: 0 }}
|
w={224}
|
||||||
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
|
boxShadow="0 0 40px 4px rgba(0, 0, 0, 0.1);"
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
>
|
>
|
||||||
@@ -170,7 +161,6 @@ export default function ProjectMenu() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Portal>
|
|
||||||
</Popover>
|
</Popover>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
@@ -186,6 +176,7 @@ const ProjectOption = ({
|
|||||||
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
|
||||||
@@ -197,8 +188,8 @@ const ProjectOption = ({
|
|||||||
}}
|
}}
|
||||||
w="full"
|
w="full"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
_hover={{ bgColor: "gray.200", textDecoration: "none" }}
|
_hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }}
|
||||||
bgColor={isActive ? "gray.100" : undefined}
|
color={isActive ? "blue.400" : undefined}
|
||||||
py={2}
|
py={2}
|
||||||
px={4}
|
px={4}
|
||||||
borderRadius={4}
|
borderRadius={4}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default function UserMenu({ user, ...rest }: { user: Session } & StackPro
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Popover placement="right">
|
<Popover placement="right">
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<NavSidebarOption>
|
<NavSidebarOption>
|
||||||
@@ -66,5 +67,6 @@ export default function UserMenu({ user, ...rest }: { user: Session } & StackPro
|
|||||||
</VStack>
|
</VStack>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
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>{label}</Text>
|
||||||
|
</HStack>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ActionButton;
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
import { useState, useEffect, useMemo } from "react";
|
|
||||||
import {
|
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
HStack,
|
|
||||||
VStack,
|
|
||||||
Icon,
|
|
||||||
Text,
|
|
||||||
Button,
|
|
||||||
Flex,
|
|
||||||
Input,
|
|
||||||
useDisclosure,
|
|
||||||
type UseDisclosureReturn,
|
|
||||||
Checkbox,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import { FiPlusSquare } from "react-icons/fi";
|
|
||||||
|
|
||||||
import { useDatasets, useHandledAsyncCallback } from "~/utils/hooks";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { useAppStore } from "~/state/store";
|
|
||||||
import ActionButton from "../ActionButton";
|
|
||||||
import InputDropdown from "../InputDropdown";
|
|
||||||
import { maybeReportError } from "~/utils/errorHandling/maybeReportError";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
const AddToDatasetButton = () => {
|
|
||||||
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
|
||||||
|
|
||||||
const disclosure = useDisclosure();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ActionButton
|
|
||||||
onClick={disclosure.onOpen}
|
|
||||||
label="Add to Dataset"
|
|
||||||
icon={FiPlusSquare}
|
|
||||||
isDisabled={selectedLogIds.size === 0}
|
|
||||||
requireBeta
|
|
||||||
/>
|
|
||||||
<AddToDatasetModal disclosure={disclosure} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddToDatasetButton;
|
|
||||||
|
|
||||||
const AddToDatasetModal = ({ disclosure }: { disclosure: UseDisclosureReturn }) => {
|
|
||||||
const selectedProjectId = useAppStore((s) => s.selectedProjectId);
|
|
||||||
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
|
||||||
const clearSelectedLogIds = useAppStore((s) => s.selectedLogs.clearSelectedLogIds);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const datasets = useDatasets().data;
|
|
||||||
|
|
||||||
const existingDatasetOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
datasets?.length
|
|
||||||
? datasets.map((d) => ({ label: d.name, id: d.id }))
|
|
||||||
: [{ label: "", id: "" }],
|
|
||||||
[datasets],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [selectedDatasetOption, setSelectedDatasetOption] = useState(existingDatasetOptions?.[0]);
|
|
||||||
const [newDatasetName, setNewDatasetName] = useState("");
|
|
||||||
const [createNewDataset, setCreateNewDataset] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (disclosure.isOpen) {
|
|
||||||
setSelectedDatasetOption(existingDatasetOptions?.[0]);
|
|
||||||
setCreateNewDataset(!existingDatasetOptions[0]?.id);
|
|
||||||
}
|
|
||||||
}, [disclosure.isOpen, existingDatasetOptions]);
|
|
||||||
|
|
||||||
const createDatasetEntriesMutation = api.datasetEntries.create.useMutation();
|
|
||||||
|
|
||||||
const [addToDataset, addingInProgress] = useHandledAsyncCallback(async () => {
|
|
||||||
if (
|
|
||||||
!selectedProjectId ||
|
|
||||||
!selectedLogIds.size ||
|
|
||||||
!(createNewDataset ? newDatasetName : selectedDatasetOption?.id)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
const datasetParams = createNewDataset
|
|
||||||
? { newDatasetParams: { projectId: selectedProjectId, name: newDatasetName } }
|
|
||||||
: { datasetId: selectedDatasetOption?.id };
|
|
||||||
const response = await createDatasetEntriesMutation.mutateAsync({
|
|
||||||
loggedCallIds: Array.from(selectedLogIds),
|
|
||||||
...datasetParams,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (maybeReportError(response)) return;
|
|
||||||
|
|
||||||
const datasetId = response.payload;
|
|
||||||
|
|
||||||
await router.push({ pathname: "/datasets/[id]", query: { id: datasetId } });
|
|
||||||
|
|
||||||
disclosure.onClose();
|
|
||||||
clearSelectedLogIds();
|
|
||||||
}, [
|
|
||||||
selectedProjectId,
|
|
||||||
selectedLogIds,
|
|
||||||
createNewDataset,
|
|
||||||
selectedDatasetOption?.id,
|
|
||||||
newDatasetName,
|
|
||||||
router,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal size={{ base: "xl", md: "2xl" }} {...disclosure}>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent w={1200}>
|
|
||||||
<ModalHeader>
|
|
||||||
<HStack>
|
|
||||||
<Icon as={FiPlusSquare} />
|
|
||||||
<Text>Add to Dataset</Text>
|
|
||||||
</HStack>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody maxW="unset">
|
|
||||||
<VStack w="full" spacing={8} pt={4} alignItems="flex-start">
|
|
||||||
<Text>
|
|
||||||
We'll add the <b>{selectedLogIds.size}</b> logs you have selected to the dataset you
|
|
||||||
choose.
|
|
||||||
</Text>
|
|
||||||
<VStack alignItems="flex-start" spacing={4}>
|
|
||||||
{existingDatasetOptions?.length && selectedDatasetOption && (
|
|
||||||
<Flex
|
|
||||||
flexDir={{ base: "column", md: "row" }}
|
|
||||||
alignItems={{ base: "flex-start", md: "center" }}
|
|
||||||
>
|
|
||||||
<Text fontWeight="bold" w={48}>
|
|
||||||
Dataset:
|
|
||||||
</Text>
|
|
||||||
<InputDropdown
|
|
||||||
options={existingDatasetOptions}
|
|
||||||
selectedOption={selectedDatasetOption}
|
|
||||||
getDisplayLabel={(option) => option.label}
|
|
||||||
onSelect={(option) => setSelectedDatasetOption(option)}
|
|
||||||
inputGroupProps={{ w: 48 }}
|
|
||||||
isDisabled={createNewDataset}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
isChecked={createNewDataset}
|
|
||||||
onChange={(e) => setCreateNewDataset(e.target.checked)}
|
|
||||||
paddingLeft={4}
|
|
||||||
isDisabled={!existingDatasetOptions[0]?.id}
|
|
||||||
>
|
|
||||||
<Text>Create New Dataset</Text>
|
|
||||||
</Checkbox>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{createNewDataset && (
|
|
||||||
<Flex
|
|
||||||
flexDir={{ base: "column", md: "row" }}
|
|
||||||
alignItems={{ base: "flex-start", md: "center" }}
|
|
||||||
>
|
|
||||||
<Text w={48} fontWeight="bold">
|
|
||||||
Dataset Name:
|
|
||||||
</Text>
|
|
||||||
<Input
|
|
||||||
w={48}
|
|
||||||
value={newDatasetName}
|
|
||||||
onChange={(e) => setNewDatasetName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</VStack>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<HStack>
|
|
||||||
<Button colorScheme="gray" onClick={disclosure.onClose} minW={24}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorScheme="blue"
|
|
||||||
onClick={addToDataset}
|
|
||||||
isLoading={addingInProgress}
|
|
||||||
minW={24}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user