mirror of
https://github.com/sst/opencode.git
synced 2025-08-09 15:58:03 +00:00
Compare commits
No commits in common. "dev" and "v0.1.157" have entirely different histories.
537 changed files with 18069 additions and 65703 deletions
|
@ -1,9 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 80
|
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
|
@ -24,4 +24,3 @@ jobs:
|
|||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
|
||||
|
|
14
.github/workflows/notify-discord.yml
vendored
14
.github/workflows/notify-discord.yml
vendored
|
@ -1,14 +0,0 @@
|
|||
name: discord
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published] # fires only when a release is published
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send nicely-formatted embed to Discord
|
||||
uses: SethCohen/github-releases-to-discord@v1
|
||||
with:
|
||||
webhook_url: ${{ secrets.DISCORD_WEBHOOK }}
|
29
.github/workflows/opencode.yml
vendored
29
.github/workflows/opencode.yml
vendored
|
@ -1,29 +0,0 @@
|
|||
name: opencode
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, ' /oc') ||
|
||||
startsWith(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, ' /opencode') ||
|
||||
startsWith(github.event.comment.body, '/opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run opencode
|
||||
uses: sst/opencode/github@latest
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
with:
|
||||
model: anthropic/claude-sonnet-4-20250514
|
30
.github/workflows/publish-github-action.yml
vendored
30
.github/workflows/publish-github-action.yml
vendored
|
@ -1,30 +0,0 @@
|
|||
name: publish-github-action
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "github-v*.*.*"
|
||||
- "!github-v1"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
./script/publish
|
||||
working-directory: ./github
|
36
.github/workflows/publish-vscode.yml
vendored
36
.github/workflows/publish-vscode.yml
vendored
|
@ -1,36 +0,0 @@
|
|||
name: publish-vscode
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "vscode-v*.*.*"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.17
|
||||
|
||||
- run: git fetch --force --tags
|
||||
- run: bun install -g @vscode/vsce
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
bun install
|
||||
./script/publish
|
||||
working-directory: ./sdks/vscode
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OPENVSX_TOKEN: ${{ secrets.OPENVSX_TOKEN }}
|
37
.github/workflows/publish.yml
vendored
37
.github/workflows/publish.yml
vendored
|
@ -1,17 +1,12 @@
|
|||
name: publish
|
||||
run-name: "${{ format('v{0}', inputs.version) }}"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish"
|
||||
required: true
|
||||
type: string
|
||||
title:
|
||||
description: "Custom title for this run"
|
||||
required: false
|
||||
type: string
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
|
@ -37,16 +32,7 @@ jobs:
|
|||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.19
|
||||
|
||||
- name: Cache ~/.bun
|
||||
id: cache-bun
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.bun
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
bun-version: 1.2.17
|
||||
|
||||
- name: Install makepkg
|
||||
run: |
|
||||
|
@ -62,12 +48,15 @@ jobs:
|
|||
git config --global user.email "opencode@sst.dev"
|
||||
git config --global user.name "opencode"
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
OPENCODE_VERSION=${{ inputs.version }} ./script/publish.ts
|
||||
bun install
|
||||
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
|
||||
./script/publish.ts
|
||||
else
|
||||
./script/publish.ts --snapshot
|
||||
fi
|
||||
working-directory: ./packages/opencode
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
|
|
34
.github/workflows/stats.yml
vendored
34
.github/workflows/stats.yml
vendored
|
@ -1,34 +0,0 @@
|
|||
name: stats
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 12 * * *" # Run daily at 12:00 UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
stats:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Run stats script
|
||||
run: bun script/stats.ts
|
||||
|
||||
- name: Commit stats
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add STATS.md
|
||||
git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
|
||||
git push
|
||||
env:
|
||||
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
24
.github/workflows/typecheck.yml
vendored
24
.github/workflows/typecheck.yml
vendored
|
@ -1,24 +0,0 @@
|
|||
name: Typecheck
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [dev]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.2.19
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Run typecheck
|
||||
run: bun typecheck
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,8 +1,7 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
.opencode
|
||||
.sst
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
openapi.json
|
||||
playground
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
description: ALWAYS use this when writing docs
|
||||
---
|
||||
|
||||
You are an expert technical documentation writer
|
||||
|
||||
You are not verbose
|
||||
|
||||
Every chunk of text should be followed by an example or something besides text
|
||||
to look at.
|
||||
|
||||
Chunks of text should not be more than 2 sentences long.
|
12
AGENTS.md
12
AGENTS.md
|
@ -1,12 +0,0 @@
|
|||
## IMPORTANT
|
||||
|
||||
- Try to keep things in one function unless composable or reusable
|
||||
- DO NOT do unnecessary destructuring of variables
|
||||
- DO NOT use `else` statements unless necessary
|
||||
- DO NOT use `try`/`catch` if it can be avoided
|
||||
- AVOID `try`/`catch` where possible
|
||||
- AVOID `else` statements
|
||||
- AVOID using `any` type
|
||||
- AVOID `let` statements
|
||||
- PREFER single word variable names where possible
|
||||
- Use as many bun apis as possible like Bun.file()
|
61
README.md
61
README.md
|
@ -9,7 +9,7 @@
|
|||
</p>
|
||||
<p align="center">AI coding agent, built for the terminal.</p>
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai/discord"><img alt="Discord" src="https://img.shields.io/discord/1391832426048651334?style=flat-square&label=discord" /></a>
|
||||
<a href="https://opencode.ai/docs"><img alt="View docs" src="https://img.shields.io/badge/view-docs-blue?style=flat-square" /></a>
|
||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
</p>
|
||||
|
@ -26,27 +26,11 @@ curl -fsSL https://opencode.ai/install | bash
|
|||
|
||||
# Package managers
|
||||
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||
brew install sst/tap/opencode # macOS and Linux
|
||||
brew install sst/tap/opencode # macOS
|
||||
paru -S opencode-bin # Arch Linux
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Remove versions older than 0.1.x before installing.
|
||||
|
||||
#### Installation Directory
|
||||
|
||||
The install script respects the following priority order for the installation path:
|
||||
|
||||
1. `$OPENCODE_INSTALL_DIR` - Custom installation directory
|
||||
2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path
|
||||
3. `$HOME/bin` - Standard user binary directory (if exists or can be created)
|
||||
4. `$HOME/.opencode/bin` - Default fallback
|
||||
|
||||
```bash
|
||||
# Examples
|
||||
OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
> **Note:** Remove versions older than 0.1.x before installing
|
||||
|
||||
### Documentation
|
||||
|
||||
|
@ -54,25 +38,7 @@ For more info on how to configure opencode [**head over to our docs**](https://o
|
|||
|
||||
### Contributing
|
||||
|
||||
opencode is an opinionated tool so any fundamental feature needs to go through a
|
||||
design process with the core team.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> We do not accept PRs for core features.
|
||||
|
||||
However we still merge a ton of PRs - you can contribute:
|
||||
|
||||
- Bug fixes
|
||||
- Improvements to LLM performance
|
||||
- Support for new providers
|
||||
- Fixes for env specific quirks
|
||||
- Missing standard behavior
|
||||
- Documentation
|
||||
|
||||
Take a look at the git history to see what kind of PRs we end up merging.
|
||||
|
||||
> [!NOTE]
|
||||
> If you do not follow the above guidelines we might close your PR.
|
||||
For any new features we'd appreciate it if you could open an issue first to discuss what you'd like to implement. We're pretty responsive there and it'll save you from working on something that we don't end up using. No need to do this for simpler fixes.
|
||||
|
||||
To run opencode locally you need.
|
||||
|
||||
|
@ -83,12 +49,19 @@ And run.
|
|||
|
||||
```bash
|
||||
$ bun install
|
||||
$ bun dev
|
||||
$ bun run packages/opencode/src/index.ts
|
||||
```
|
||||
|
||||
#### Development Notes
|
||||
|
||||
**API Client**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you will need the opencode team to generate a new stainless sdk for the clients.
|
||||
**API Client Generation**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you need to regenerate the Go client and OpenAPI specification:
|
||||
|
||||
```bash
|
||||
$ cd packages/tui
|
||||
$ go generate ./pkg/client/
|
||||
```
|
||||
|
||||
This updates the generated Go client code that the TUI uses to communicate with the backend server.
|
||||
|
||||
### FAQ
|
||||
|
||||
|
@ -97,14 +70,18 @@ $ bun dev
|
|||
It's very similar to Claude Code in terms of capability. Here are the key differences:
|
||||
|
||||
- 100% open source
|
||||
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider-agnostic is important.
|
||||
- Not coupled to any provider. Although Anthropic is recommended, opencode can be used with OpenAI, Google or even local models. As models evolve the gaps between them will close and pricing will drop so being provider agnostic is important.
|
||||
- A focus on TUI. opencode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal.
|
||||
- A client/server architecture. This for example can allow opencode to run on your computer, while you can drive it remotely from a mobile app. Meaning that the TUI frontend is just one of the possible clients.
|
||||
|
||||
#### What about Windows support?
|
||||
|
||||
There are some minor problems blocking opencode from working on windows. We are working on on them now. You'll need to use WSL for now.
|
||||
|
||||
#### What's the other repo?
|
||||
|
||||
The other confusingly named repo has no relation to this one. You can [read the story behind it here](https://x.com/thdxr/status/1933561254481666466).
|
||||
|
||||
---
|
||||
|
||||
**Join our community** [Discord](https://discord.gg/opencode) | [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
|
||||
**Join our community** [YouTube](https://www.youtube.com/c/sst-dev) | [X.com](https://x.com/SST_dev)
|
||||
|
|
45
STATS.md
45
STATS.md
|
@ -1,45 +0,0 @@
|
|||
# Download Stats
|
||||
|
||||
| Date | GitHub Downloads | npm Downloads | Total |
|
||||
| ---------- | ---------------- | ---------------- | ---------------- |
|
||||
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
|
||||
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
|
||||
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
|
||||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
|
||||
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
|
||||
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
|
||||
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
|
||||
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
|
||||
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
|
||||
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
|
||||
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
|
||||
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
|
||||
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
|
||||
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
|
||||
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
|
||||
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
|
||||
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
|
||||
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
|
||||
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
|
||||
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
|
||||
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
|
||||
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
|
||||
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
|
||||
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
|
||||
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
|
||||
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
|
||||
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
|
||||
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
|
||||
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
|
||||
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
|
||||
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
|
||||
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
|
||||
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
|
||||
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
|
||||
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
|
||||
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
|
|
@ -1,20 +0,0 @@
|
|||
import { defineConfig } from "drizzle-kit"
|
||||
import { Resource } from "sst"
|
||||
|
||||
export default defineConfig({
|
||||
out: "./migrations/",
|
||||
strict: true,
|
||||
schema: ["./src/**/*.sql.ts"],
|
||||
verbose: true,
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
database: Resource.Database.database,
|
||||
host: Resource.Database.host,
|
||||
user: Resource.Database.username,
|
||||
password: Resource.Database.password,
|
||||
port: Resource.Database.port,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
},
|
||||
})
|
|
@ -1,66 +0,0 @@
|
|||
CREATE TABLE "billing" (
|
||||
"id" varchar(30) NOT NULL,
|
||||
"workspace_id" varchar(30) NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"customer_id" varchar(255),
|
||||
"payment_method_id" varchar(255),
|
||||
"payment_method_last4" varchar(4),
|
||||
"balance" bigint NOT NULL,
|
||||
"reload" boolean,
|
||||
CONSTRAINT "billing_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "payment" (
|
||||
"id" varchar(30) NOT NULL,
|
||||
"workspace_id" varchar(30) NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"customer_id" varchar(255),
|
||||
"payment_id" varchar(255),
|
||||
"amount" bigint NOT NULL,
|
||||
CONSTRAINT "payment_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "usage" (
|
||||
"id" varchar(30) NOT NULL,
|
||||
"workspace_id" varchar(30) NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"request_id" varchar(255),
|
||||
"model" varchar(255) NOT NULL,
|
||||
"input_tokens" integer NOT NULL,
|
||||
"output_tokens" integer NOT NULL,
|
||||
"reasoning_tokens" integer,
|
||||
"cache_read_tokens" integer,
|
||||
"cache_write_tokens" integer,
|
||||
"cost" bigint NOT NULL,
|
||||
CONSTRAINT "usage_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user" (
|
||||
"id" varchar(30) NOT NULL,
|
||||
"workspace_id" varchar(30) NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"email" text NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"time_seen" timestamp with time zone,
|
||||
"color" integer,
|
||||
CONSTRAINT "user_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "workspace" (
|
||||
"id" varchar(30) PRIMARY KEY NOT NULL,
|
||||
"slug" varchar(255),
|
||||
"name" varchar(255),
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "billing" ADD CONSTRAINT "billing_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "payment" ADD CONSTRAINT "payment_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "usage" ADD CONSTRAINT "usage_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD CONSTRAINT "user_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "user_email" ON "user" USING btree ("workspace_id","email");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "slug" ON "workspace" USING btree ("slug");
|
|
@ -1,8 +0,0 @@
|
|||
CREATE TABLE "account" (
|
||||
"id" varchar(30) NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"email" varchar(255) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "email" ON "account" USING btree ("email");
|
|
@ -1,14 +0,0 @@
|
|||
CREATE TABLE "key" (
|
||||
"id" varchar(30) NOT NULL,
|
||||
"workspace_id" varchar(30) NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"user_id" text NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"key" varchar(255) NOT NULL,
|
||||
"time_used" timestamp with time zone,
|
||||
CONSTRAINT "key_workspace_id_id_pk" PRIMARY KEY("workspace_id","id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "key" ADD CONSTRAINT "key_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "global_key" ON "key" USING btree ("key");
|
|
@ -1 +0,0 @@
|
|||
ALTER TABLE "usage" DROP COLUMN "request_id";
|
|
@ -1,461 +0,0 @@
|
|||
{
|
||||
"id": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.billing": {
|
||||
"name": "billing",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_method_id": {
|
||||
"name": "payment_method_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_method_last4": {
|
||||
"name": "payment_method_last4",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"reload": {
|
||||
"name": "reload",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"billing_workspace_id_workspace_id_fk": {
|
||||
"name": "billing_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "billing",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"billing_workspace_id_id_pk": {
|
||||
"name": "billing_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.payment": {
|
||||
"name": "payment",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"payment_workspace_id_workspace_id_fk": {
|
||||
"name": "payment_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "payment",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"payment_workspace_id_id_pk": {
|
||||
"name": "payment_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.usage": {
|
||||
"name": "usage",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"request_id": {
|
||||
"name": "request_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"reasoning_tokens": {
|
||||
"name": "reasoning_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"name": "cache_read_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cache_write_tokens": {
|
||||
"name": "cache_write_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"usage_workspace_id_workspace_id_fk": {
|
||||
"name": "usage_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "usage",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "workspace_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_workspace_id_workspace_id_fk": {
|
||||
"name": "user_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "user",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_workspace_id_id_pk": {
|
||||
"name": "user_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workspace": {
|
||||
"name": "workspace",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
|
@ -1,515 +0,0 @@
|
|||
{
|
||||
"id": "bf9e9084-4073-4ecb-8e56-5610816c9589",
|
||||
"prevId": "9b5cec8c-8b59-4d7a-bb5c-76ade1c83d6f",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.billing": {
|
||||
"name": "billing",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_method_id": {
|
||||
"name": "payment_method_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_method_last4": {
|
||||
"name": "payment_method_last4",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"reload": {
|
||||
"name": "reload",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"billing_workspace_id_workspace_id_fk": {
|
||||
"name": "billing_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "billing",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"billing_workspace_id_id_pk": {
|
||||
"name": "billing_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.payment": {
|
||||
"name": "payment",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"payment_workspace_id_workspace_id_fk": {
|
||||
"name": "payment_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "payment",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"payment_workspace_id_id_pk": {
|
||||
"name": "payment_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.usage": {
|
||||
"name": "usage",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"request_id": {
|
||||
"name": "request_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"reasoning_tokens": {
|
||||
"name": "reasoning_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"name": "cache_read_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cache_write_tokens": {
|
||||
"name": "cache_write_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"usage_workspace_id_workspace_id_fk": {
|
||||
"name": "usage_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "usage",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "workspace_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_workspace_id_workspace_id_fk": {
|
||||
"name": "user_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "user",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_workspace_id_id_pk": {
|
||||
"name": "user_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workspace": {
|
||||
"name": "workspace",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
|
@ -1,615 +0,0 @@
|
|||
{
|
||||
"id": "351e4956-74e0-4282-a23b-02f1a73fa38c",
|
||||
"prevId": "bf9e9084-4073-4ecb-8e56-5610816c9589",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.billing": {
|
||||
"name": "billing",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_method_id": {
|
||||
"name": "payment_method_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_method_last4": {
|
||||
"name": "payment_method_last4",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"reload": {
|
||||
"name": "reload",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"billing_workspace_id_workspace_id_fk": {
|
||||
"name": "billing_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "billing",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"billing_workspace_id_id_pk": {
|
||||
"name": "billing_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.payment": {
|
||||
"name": "payment",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"payment_workspace_id_workspace_id_fk": {
|
||||
"name": "payment_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "payment",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"payment_workspace_id_id_pk": {
|
||||
"name": "payment_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.usage": {
|
||||
"name": "usage",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"request_id": {
|
||||
"name": "request_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"reasoning_tokens": {
|
||||
"name": "reasoning_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"name": "cache_read_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cache_write_tokens": {
|
||||
"name": "cache_write_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"usage_workspace_id_workspace_id_fk": {
|
||||
"name": "usage_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "usage",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.key": {
|
||||
"name": "key",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_used": {
|
||||
"name": "time_used",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_key": {
|
||||
"name": "global_key",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "key",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"key_workspace_id_workspace_id_fk": {
|
||||
"name": "key_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "key",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"key_workspace_id_id_pk": {
|
||||
"name": "key_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "workspace_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_workspace_id_workspace_id_fk": {
|
||||
"name": "user_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "user",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_workspace_id_id_pk": {
|
||||
"name": "user_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workspace": {
|
||||
"name": "workspace",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
|
@ -1,609 +0,0 @@
|
|||
{
|
||||
"id": "fa935883-9e51-4811-90c7-8967eefe458c",
|
||||
"prevId": "351e4956-74e0-4282-a23b-02f1a73fa38c",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email": {
|
||||
"name": "email",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.billing": {
|
||||
"name": "billing",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_method_id": {
|
||||
"name": "payment_method_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_method_last4": {
|
||||
"name": "payment_method_last4",
|
||||
"type": "varchar(4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"balance": {
|
||||
"name": "balance",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"reload": {
|
||||
"name": "reload",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"billing_workspace_id_workspace_id_fk": {
|
||||
"name": "billing_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "billing",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"billing_workspace_id_id_pk": {
|
||||
"name": "billing_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.payment": {
|
||||
"name": "payment",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customer_id": {
|
||||
"name": "customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"payment_id": {
|
||||
"name": "payment_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"amount": {
|
||||
"name": "amount",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"payment_workspace_id_workspace_id_fk": {
|
||||
"name": "payment_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "payment",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"payment_workspace_id_id_pk": {
|
||||
"name": "payment_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.usage": {
|
||||
"name": "usage",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"input_tokens": {
|
||||
"name": "input_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"output_tokens": {
|
||||
"name": "output_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"reasoning_tokens": {
|
||||
"name": "reasoning_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cache_read_tokens": {
|
||||
"name": "cache_read_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cache_write_tokens": {
|
||||
"name": "cache_write_tokens",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"usage_workspace_id_workspace_id_fk": {
|
||||
"name": "usage_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "usage",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"usage_workspace_id_id_pk": {
|
||||
"name": "usage_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.key": {
|
||||
"name": "key",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_used": {
|
||||
"name": "time_used",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"global_key": {
|
||||
"name": "global_key",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "key",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"key_workspace_id_workspace_id_fk": {
|
||||
"name": "key_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "key",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"key_workspace_id_id_pk": {
|
||||
"name": "key_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"workspace_id": {
|
||||
"name": "workspace_id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "workspace_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_workspace_id_workspace_id_fk": {
|
||||
"name": "user_workspace_id_workspace_id_fk",
|
||||
"tableFrom": "user",
|
||||
"tableTo": "workspace",
|
||||
"columnsFrom": [
|
||||
"workspace_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_workspace_id_id_pk": {
|
||||
"name": "user_workspace_id_id_pk",
|
||||
"columns": [
|
||||
"workspace_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.workspace": {
|
||||
"name": "workspace",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1754518198186,
|
||||
"tag": "0000_amused_mojo",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1754609655262,
|
||||
"tag": "0001_thankful_chat",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1754627626945,
|
||||
"tag": "0002_stale_jackal",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1754672464106,
|
||||
"tag": "0003_tranquil_spencer_smythe",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode/cloud-core",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"drizzle-orm": "0.41.0",
|
||||
"postgres": "3.4.7",
|
||||
"stripe": "18.0.0",
|
||||
"ulid": "3.0.0"
|
||||
},
|
||||
"exports": {
|
||||
"./*": "./src/*"
|
||||
},
|
||||
"scripts": {
|
||||
"db": "sst shell drizzle-kit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"drizzle-kit": "0.30.5"
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
import { z } from "zod"
|
||||
import { and, eq, getTableColumns, isNull } from "drizzle-orm"
|
||||
import { fn } from "./util/fn"
|
||||
import { Database } from "./drizzle"
|
||||
import { Identifier } from "./identifier"
|
||||
import { AccountTable } from "./schema/account.sql"
|
||||
import { Actor } from "./actor"
|
||||
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||
import { UserTable } from "./schema/user.sql"
|
||||
|
||||
export namespace Account {
|
||||
export const create = fn(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
id: z.string().optional(),
|
||||
}),
|
||||
async (input) =>
|
||||
Database.transaction(async (tx) => {
|
||||
const id = input.id ?? Identifier.create("account")
|
||||
await tx.insert(AccountTable).values({
|
||||
id,
|
||||
email: input.email,
|
||||
})
|
||||
return id
|
||||
}),
|
||||
)
|
||||
|
||||
export const fromID = fn(z.string(), async (id) =>
|
||||
Database.transaction(async (tx) => {
|
||||
return tx
|
||||
.select()
|
||||
.from(AccountTable)
|
||||
.where(eq(AccountTable.id, id))
|
||||
.execute()
|
||||
.then((rows) => rows[0])
|
||||
}),
|
||||
)
|
||||
|
||||
export const fromEmail = fn(z.string().email(), async (email) =>
|
||||
Database.transaction(async (tx) => {
|
||||
return tx
|
||||
.select()
|
||||
.from(AccountTable)
|
||||
.where(eq(AccountTable.email, email))
|
||||
.execute()
|
||||
.then((rows) => rows[0])
|
||||
}),
|
||||
)
|
||||
|
||||
export const workspaces = async () => {
|
||||
const actor = Actor.assert("account")
|
||||
return Database.transaction(async (tx) =>
|
||||
tx
|
||||
.select(getTableColumns(WorkspaceTable))
|
||||
.from(WorkspaceTable)
|
||||
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.email, actor.properties.email),
|
||||
isNull(UserTable.timeDeleted),
|
||||
isNull(WorkspaceTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.execute(),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
import { Context } from "./context"
|
||||
import { Log } from "./util/log"
|
||||
|
||||
export namespace Actor {
|
||||
interface Account {
|
||||
type: "account"
|
||||
properties: {
|
||||
accountID: string
|
||||
email: string
|
||||
}
|
||||
}
|
||||
|
||||
interface Public {
|
||||
type: "public"
|
||||
properties: {}
|
||||
}
|
||||
|
||||
interface User {
|
||||
type: "user"
|
||||
properties: {
|
||||
userID: string
|
||||
workspaceID: string
|
||||
email: string
|
||||
}
|
||||
}
|
||||
|
||||
interface System {
|
||||
type: "system"
|
||||
properties: {
|
||||
workspaceID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type Info = Account | Public | User | System
|
||||
|
||||
const ctx = Context.create<Info>()
|
||||
export const use = ctx.use
|
||||
|
||||
const log = Log.create().tag("namespace", "actor")
|
||||
|
||||
export function provide<R, T extends Info["type"]>(
|
||||
type: T,
|
||||
properties: Extract<Info, { type: T }>["properties"],
|
||||
cb: () => R,
|
||||
) {
|
||||
return ctx.provide(
|
||||
{
|
||||
type,
|
||||
properties,
|
||||
} as any,
|
||||
() => {
|
||||
return Log.provide({ ...properties }, () => {
|
||||
log.info("provided")
|
||||
return cb()
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function assert<T extends Info["type"]>(type: T) {
|
||||
const actor = use()
|
||||
if (actor.type !== type) {
|
||||
throw new Error(`Expected actor type ${type}, got ${actor.type}`)
|
||||
}
|
||||
return actor as Extract<Info, { type: T }>
|
||||
}
|
||||
|
||||
export function workspace() {
|
||||
const actor = use()
|
||||
if ("workspaceID" in actor.properties) {
|
||||
return actor.properties.workspaceID
|
||||
}
|
||||
throw new Error(`actor of type "${actor.type}" is not associated with a workspace`)
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import { Resource } from "sst"
|
||||
import { Stripe } from "stripe"
|
||||
import { Database, eq, sql } from "./drizzle"
|
||||
import { BillingTable, UsageTable } from "./schema/billing.sql"
|
||||
import { Actor } from "./actor"
|
||||
import { fn } from "./util/fn"
|
||||
import { z } from "zod"
|
||||
import { Identifier } from "./identifier"
|
||||
import { centsToMicroCents } from "./util/price"
|
||||
|
||||
export namespace Billing {
|
||||
export const stripe = () =>
|
||||
new Stripe(Resource.STRIPE_SECRET_KEY.value, {
|
||||
apiVersion: "2025-03-31.basil",
|
||||
})
|
||||
|
||||
export const get = async () => {
|
||||
return Database.use(async (tx) =>
|
||||
tx
|
||||
.select({
|
||||
customerID: BillingTable.customerID,
|
||||
paymentMethodID: BillingTable.paymentMethodID,
|
||||
balance: BillingTable.balance,
|
||||
reload: BillingTable.reload,
|
||||
})
|
||||
.from(BillingTable)
|
||||
.where(eq(BillingTable.workspaceID, Actor.workspace()))
|
||||
.then((r) => r[0]),
|
||||
)
|
||||
}
|
||||
|
||||
export const consume = fn(
|
||||
z.object({
|
||||
requestID: z.string().optional(),
|
||||
model: z.string(),
|
||||
inputTokens: z.number(),
|
||||
outputTokens: z.number(),
|
||||
reasoningTokens: z.number().optional(),
|
||||
cacheReadTokens: z.number().optional(),
|
||||
cacheWriteTokens: z.number().optional(),
|
||||
costInCents: z.number(),
|
||||
}),
|
||||
async (input) => {
|
||||
const workspaceID = Actor.workspace()
|
||||
const cost = centsToMicroCents(input.costInCents)
|
||||
|
||||
return await Database.transaction(async (tx) => {
|
||||
await tx.insert(UsageTable).values({
|
||||
workspaceID,
|
||||
id: Identifier.create("usage"),
|
||||
requestID: input.requestID,
|
||||
model: input.model,
|
||||
inputTokens: input.inputTokens,
|
||||
outputTokens: input.outputTokens,
|
||||
reasoningTokens: input.reasoningTokens,
|
||||
cacheReadTokens: input.cacheReadTokens,
|
||||
cacheWriteTokens: input.cacheWriteTokens,
|
||||
cost,
|
||||
})
|
||||
const [updated] = await tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
balance: sql`${BillingTable.balance} - ${cost}`,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
.returning()
|
||||
return updated.balance
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import { AsyncLocalStorage } from "node:async_hooks"
|
||||
|
||||
export namespace Context {
|
||||
export class NotFound extends Error {}
|
||||
|
||||
export function create<T>() {
|
||||
const storage = new AsyncLocalStorage<T>()
|
||||
return {
|
||||
use() {
|
||||
const result = storage.getStore()
|
||||
if (!result) {
|
||||
throw new NotFound()
|
||||
}
|
||||
return result
|
||||
},
|
||||
provide<R>(value: T, fn: () => R) {
|
||||
return storage.run<R>(value, fn)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
import { drizzle } from "drizzle-orm/postgres-js"
|
||||
import { Resource } from "sst"
|
||||
export * from "drizzle-orm"
|
||||
import postgres from "postgres"
|
||||
|
||||
function createClient() {
|
||||
const client = postgres({
|
||||
idle_timeout: 30000,
|
||||
connect_timeout: 30000,
|
||||
host: Resource.Database.host,
|
||||
database: Resource.Database.database,
|
||||
user: Resource.Database.username,
|
||||
password: Resource.Database.password,
|
||||
port: Resource.Database.port,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
max: 1,
|
||||
})
|
||||
|
||||
return drizzle(client, {})
|
||||
}
|
||||
|
||||
import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
|
||||
import type { ExtractTablesWithRelations } from "drizzle-orm"
|
||||
import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"
|
||||
import { Context } from "../context"
|
||||
|
||||
export namespace Database {
|
||||
export type Transaction = PgTransaction<
|
||||
PostgresJsQueryResultHKT,
|
||||
Record<string, unknown>,
|
||||
ExtractTablesWithRelations<Record<string, unknown>>
|
||||
>
|
||||
|
||||
export type TxOrDb = Transaction | ReturnType<typeof createClient>
|
||||
|
||||
const TransactionContext = Context.create<{
|
||||
tx: TxOrDb
|
||||
effects: (() => void | Promise<void>)[]
|
||||
}>()
|
||||
|
||||
export async function use<T>(callback: (trx: TxOrDb) => Promise<T>) {
|
||||
try {
|
||||
const { tx } = TransactionContext.use()
|
||||
return tx.transaction(callback)
|
||||
} catch (err) {
|
||||
if (err instanceof Context.NotFound) {
|
||||
const client = createClient()
|
||||
const effects: (() => void | Promise<void>)[] = []
|
||||
const result = await TransactionContext.provide(
|
||||
{
|
||||
effects,
|
||||
tx: client,
|
||||
},
|
||||
() => callback(client),
|
||||
)
|
||||
await Promise.all(effects.map((x) => x()))
|
||||
return result
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
export async function fn<Input, T>(callback: (input: Input, trx: TxOrDb) => Promise<T>) {
|
||||
return (input: Input) => use(async (tx) => callback(input, tx))
|
||||
}
|
||||
|
||||
export async function effect(effect: () => any | Promise<any>) {
|
||||
try {
|
||||
const { effects } = TransactionContext.use()
|
||||
effects.push(effect)
|
||||
} catch {
|
||||
await effect()
|
||||
}
|
||||
}
|
||||
|
||||
export async function transaction<T>(callback: (tx: TxOrDb) => Promise<T>, config?: PgTransactionConfig) {
|
||||
try {
|
||||
const { tx } = TransactionContext.use()
|
||||
return callback(tx)
|
||||
} catch (err) {
|
||||
if (err instanceof Context.NotFound) {
|
||||
const client = createClient()
|
||||
const effects: (() => void | Promise<void>)[] = []
|
||||
const result = await client.transaction(async (tx) => {
|
||||
return TransactionContext.provide({ tx, effects }, () => callback(tx))
|
||||
}, config)
|
||||
await Promise.all(effects.map((x) => x()))
|
||||
return result
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
import { bigint, timestamp, varchar } from "drizzle-orm/pg-core"
|
||||
|
||||
export const ulid = (name: string) => varchar(name, { length: 30 })
|
||||
|
||||
export const workspaceColumns = {
|
||||
get id() {
|
||||
return ulid("id").notNull()
|
||||
},
|
||||
get workspaceID() {
|
||||
return ulid("workspace_id").notNull()
|
||||
},
|
||||
}
|
||||
|
||||
export const id = () => ulid("id").notNull()
|
||||
|
||||
export const utc = (name: string) =>
|
||||
timestamp(name, {
|
||||
withTimezone: true,
|
||||
})
|
||||
|
||||
export const currency = (name: string) =>
|
||||
bigint(name, {
|
||||
mode: "number",
|
||||
})
|
||||
|
||||
export const timestamps = {
|
||||
timeCreated: utc("time_created").notNull().defaultNow(),
|
||||
timeDeleted: utc("time_deleted"),
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { ulid } from "ulid"
|
||||
import { z } from "zod"
|
||||
|
||||
export namespace Identifier {
|
||||
const prefixes = {
|
||||
account: "acc",
|
||||
billing: "bil",
|
||||
key: "key",
|
||||
payment: "pay",
|
||||
usage: "usg",
|
||||
user: "usr",
|
||||
workspace: "wrk",
|
||||
} as const
|
||||
|
||||
export function create(prefix: keyof typeof prefixes, given?: string): string {
|
||||
if (given) {
|
||||
if (given.startsWith(prefixes[prefix])) return given
|
||||
throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`)
|
||||
}
|
||||
return [prefixes[prefix], ulid()].join("_")
|
||||
}
|
||||
|
||||
export function schema(prefix: keyof typeof prefixes) {
|
||||
return z.string().startsWith(prefixes[prefix])
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import { pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
|
||||
import { id, timestamps } from "../drizzle/types"
|
||||
|
||||
export const AccountTable = pgTable(
|
||||
"account",
|
||||
{
|
||||
id: id(),
|
||||
...timestamps,
|
||||
email: varchar("email", { length: 255 }).notNull(),
|
||||
},
|
||||
(table) => [uniqueIndex("email").on(table.email)],
|
||||
)
|
|
@ -1,45 +0,0 @@
|
|||
import { bigint, boolean, integer, pgTable, varchar } from "drizzle-orm/pg-core"
|
||||
import { timestamps, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
export const BillingTable = pgTable(
|
||||
"billing",
|
||||
{
|
||||
...workspaceColumns,
|
||||
...timestamps,
|
||||
customerID: varchar("customer_id", { length: 255 }),
|
||||
paymentMethodID: varchar("payment_method_id", { length: 255 }),
|
||||
paymentMethodLast4: varchar("payment_method_last4", { length: 4 }),
|
||||
balance: bigint("balance", { mode: "number" }).notNull(),
|
||||
reload: boolean("reload"),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table)],
|
||||
)
|
||||
|
||||
export const PaymentTable = pgTable(
|
||||
"payment",
|
||||
{
|
||||
...workspaceColumns,
|
||||
...timestamps,
|
||||
customerID: varchar("customer_id", { length: 255 }),
|
||||
paymentID: varchar("payment_id", { length: 255 }),
|
||||
amount: bigint("amount", { mode: "number" }).notNull(),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table)],
|
||||
)
|
||||
|
||||
export const UsageTable = pgTable(
|
||||
"usage",
|
||||
{
|
||||
...workspaceColumns,
|
||||
...timestamps,
|
||||
model: varchar("model", { length: 255 }).notNull(),
|
||||
inputTokens: integer("input_tokens").notNull(),
|
||||
outputTokens: integer("output_tokens").notNull(),
|
||||
reasoningTokens: integer("reasoning_tokens"),
|
||||
cacheReadTokens: integer("cache_read_tokens"),
|
||||
cacheWriteTokens: integer("cache_write_tokens"),
|
||||
cost: bigint("cost", { mode: "number" }).notNull(),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table)],
|
||||
)
|
|
@ -1,16 +0,0 @@
|
|||
import { text, pgTable, varchar, uniqueIndex } from "drizzle-orm/pg-core"
|
||||
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
export const KeyTable = pgTable(
|
||||
"key",
|
||||
{
|
||||
...workspaceColumns,
|
||||
...timestamps,
|
||||
userID: text("user_id").notNull(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
key: varchar("key", { length: 255 }).notNull(),
|
||||
timeUsed: utc("time_used"),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table), uniqueIndex("global_key").on(table.key)],
|
||||
)
|
|
@ -1,16 +0,0 @@
|
|||
import { text, pgTable, uniqueIndex, varchar, integer } from "drizzle-orm/pg-core"
|
||||
import { timestamps, utc, workspaceColumns } from "../drizzle/types"
|
||||
import { workspaceIndexes } from "./workspace.sql"
|
||||
|
||||
export const UserTable = pgTable(
|
||||
"user",
|
||||
{
|
||||
...workspaceColumns,
|
||||
...timestamps,
|
||||
email: text("email").notNull(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
timeSeen: utc("time_seen"),
|
||||
color: integer("color"),
|
||||
},
|
||||
(table) => [...workspaceIndexes(table), uniqueIndex("user_email").on(table.workspaceID, table.email)],
|
||||
)
|
|
@ -1,25 +0,0 @@
|
|||
import { primaryKey, foreignKey, pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core"
|
||||
import { timestamps, ulid } from "../drizzle/types"
|
||||
|
||||
export const WorkspaceTable = pgTable(
|
||||
"workspace",
|
||||
{
|
||||
id: ulid("id").notNull().primaryKey(),
|
||||
slug: varchar("slug", { length: 255 }),
|
||||
name: varchar("name", { length: 255 }),
|
||||
...timestamps,
|
||||
},
|
||||
(table) => [uniqueIndex("slug").on(table.slug)],
|
||||
)
|
||||
|
||||
export function workspaceIndexes(table: any) {
|
||||
return [
|
||||
primaryKey({
|
||||
columns: [table.workspaceID, table.id],
|
||||
}),
|
||||
foreignKey({
|
||||
foreignColumns: [WorkspaceTable.id],
|
||||
columns: [table.workspaceID],
|
||||
}),
|
||||
]
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import { z } from "zod"
|
||||
|
||||
export function fn<T extends z.ZodType, Result>(
|
||||
schema: T,
|
||||
cb: (input: z.output<T>) => Result,
|
||||
) {
|
||||
const result = (input: z.input<T>) => {
|
||||
const parsed = schema.parse(input)
|
||||
return cb(parsed)
|
||||
}
|
||||
result.force = (input: z.input<T>) => cb(input)
|
||||
result.schema = schema
|
||||
return result
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import { Context } from "../context"
|
||||
|
||||
export namespace Log {
|
||||
const ctx = Context.create<{
|
||||
tags: Record<string, any>
|
||||
}>()
|
||||
|
||||
export function create(tags?: Record<string, any>) {
|
||||
tags = tags || {}
|
||||
|
||||
const result = {
|
||||
info(message?: any, extra?: Record<string, any>) {
|
||||
const prefix = Object.entries({
|
||||
...use().tags,
|
||||
...tags,
|
||||
...extra,
|
||||
})
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join(" ")
|
||||
console.log(prefix, message)
|
||||
return result
|
||||
},
|
||||
tag(key: string, value: string) {
|
||||
if (tags) tags[key] = value
|
||||
return result
|
||||
},
|
||||
clone() {
|
||||
return Log.create({ ...tags })
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function provide<R>(tags: Record<string, any>, cb: () => R) {
|
||||
const existing = use()
|
||||
return ctx.provide(
|
||||
{
|
||||
tags: {
|
||||
...existing.tags,
|
||||
...tags,
|
||||
},
|
||||
},
|
||||
cb,
|
||||
)
|
||||
}
|
||||
|
||||
function use() {
|
||||
try {
|
||||
return ctx.use()
|
||||
} catch (e) {
|
||||
return { tags: {} }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export function centsToMicroCents(amount: number) {
|
||||
return Math.round(amount * 1000000)
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import { z } from "zod"
|
||||
import { fn } from "./util/fn"
|
||||
import { centsToMicroCents } from "./util/price"
|
||||
import { Actor } from "./actor"
|
||||
import { Database, eq } from "./drizzle"
|
||||
import { Identifier } from "./identifier"
|
||||
import { UserTable } from "./schema/user.sql"
|
||||
import { BillingTable } from "./schema/billing.sql"
|
||||
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||
|
||||
export namespace Workspace {
|
||||
export const create = fn(z.void(), async () => {
|
||||
const account = Actor.assert("account")
|
||||
const workspaceID = Identifier.create("workspace")
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx.insert(WorkspaceTable).values({
|
||||
id: workspaceID,
|
||||
})
|
||||
await tx.insert(UserTable).values({
|
||||
workspaceID,
|
||||
id: Identifier.create("user"),
|
||||
email: account.properties.email,
|
||||
name: "",
|
||||
})
|
||||
await tx.insert(BillingTable).values({
|
||||
workspaceID,
|
||||
id: Identifier.create("billing"),
|
||||
balance: centsToMicroCents(100),
|
||||
})
|
||||
})
|
||||
return workspaceID
|
||||
})
|
||||
|
||||
export async function list() {
|
||||
const account = Actor.assert("account")
|
||||
return Database.use(async (tx) => {
|
||||
return tx
|
||||
.select({
|
||||
id: WorkspaceTable.id,
|
||||
slug: WorkspaceTable.slug,
|
||||
name: WorkspaceTable.name,
|
||||
})
|
||||
.from(UserTable)
|
||||
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||
.where(eq(UserTable.email, account.properties.email))
|
||||
})
|
||||
}
|
||||
}
|
9
cloud/core/sst-env.d.ts
vendored
9
cloud/core/sst-env.d.ts
vendored
|
@ -1,9 +0,0 @@
|
|||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["@cloudflare/workers-types", "node"]
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"name": "@opencode/cloud-function",
|
||||
"version": "0.3.130",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "4.20250522.0",
|
||||
"@types/node": "catalog:",
|
||||
"openai": "5.11.0",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
"@ai-sdk/openai-compatible": "1.0.1",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||
"ai": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
import { Resource } from "sst"
|
||||
import { z } from "zod"
|
||||
import { issuer } from "@openauthjs/openauth"
|
||||
import { createSubjects } from "@openauthjs/openauth/subject"
|
||||
import { CodeProvider } from "@openauthjs/openauth/provider/code"
|
||||
import { GithubProvider } from "@openauthjs/openauth/provider/github"
|
||||
import { GoogleOidcProvider } from "@openauthjs/openauth/provider/google"
|
||||
import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
|
||||
import { Account } from "@opencode/cloud-core/account.js"
|
||||
|
||||
type Env = {
|
||||
AuthStorage: KVNamespace
|
||||
}
|
||||
|
||||
export const subjects = createSubjects({
|
||||
account: z.object({
|
||||
accountID: z.string(),
|
||||
email: z.string(),
|
||||
}),
|
||||
user: z.object({
|
||||
userID: z.string(),
|
||||
workspaceID: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
||||
return issuer({
|
||||
providers: {
|
||||
github: GithubProvider({
|
||||
clientID: Resource.GITHUB_CLIENT_ID_CONSOLE.value,
|
||||
clientSecret: Resource.GITHUB_CLIENT_SECRET_CONSOLE.value,
|
||||
scopes: ["read:user", "user:email"],
|
||||
}),
|
||||
google: GoogleOidcProvider({
|
||||
clientID: Resource.GOOGLE_CLIENT_ID.value,
|
||||
scopes: ["openid", "email"],
|
||||
}),
|
||||
// email: CodeProvider({
|
||||
// async request(req, state, form, error) {
|
||||
// console.log(state)
|
||||
// const params = new URLSearchParams()
|
||||
// if (error) {
|
||||
// params.set("error", error.type)
|
||||
// }
|
||||
// if (state.type === "start") {
|
||||
// return Response.redirect(process.env.AUTH_FRONTEND_URL + "/auth/email?" + params.toString(), 302)
|
||||
// }
|
||||
//
|
||||
// if (state.type === "code") {
|
||||
// return Response.redirect(process.env.AUTH_FRONTEND_URL + "/auth/code?" + params.toString(), 302)
|
||||
// }
|
||||
//
|
||||
// return new Response("ok")
|
||||
// },
|
||||
// async sendCode(claims, code) {
|
||||
// const email = z.string().email().parse(claims.email)
|
||||
// const cmd = new SendEmailCommand({
|
||||
// Destination: {
|
||||
// ToAddresses: [email],
|
||||
// },
|
||||
// FromEmailAddress: `SST <auth@${Resource.Email.sender}>`,
|
||||
// Content: {
|
||||
// Simple: {
|
||||
// Body: {
|
||||
// Html: {
|
||||
// Data: `Your pin code is <strong>${code}</strong>`,
|
||||
// },
|
||||
// Text: {
|
||||
// Data: `Your pin code is ${code}`,
|
||||
// },
|
||||
// },
|
||||
// Subject: {
|
||||
// Data: "SST Console Pin Code: " + code,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// await ses.send(cmd)
|
||||
// },
|
||||
// }),
|
||||
},
|
||||
storage: CloudflareStorage({
|
||||
namespace: env.AuthStorage,
|
||||
}),
|
||||
subjects,
|
||||
async success(ctx, response) {
|
||||
console.log(response)
|
||||
|
||||
let email: string | undefined
|
||||
|
||||
if (response.provider === "github") {
|
||||
const userResponse = await fetch("https://api.github.com/user", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${response.tokenset.access}`,
|
||||
"User-Agent": "opencode",
|
||||
Accept: "application/vnd.github+json",
|
||||
},
|
||||
})
|
||||
const user = (await userResponse.json()) as { email: string }
|
||||
email = user.email
|
||||
} else if (response.provider === "google") {
|
||||
if (!response.id.email_verified) throw new Error("Google email not verified")
|
||||
email = response.id.email as string
|
||||
}
|
||||
//if (response.provider === "email") {
|
||||
// email = response.claims.email
|
||||
//}
|
||||
else throw new Error("Unsupported provider")
|
||||
|
||||
if (!email) throw new Error("No email found")
|
||||
|
||||
let accountID = await Account.fromEmail(email).then((x) => x?.id)
|
||||
if (!accountID) {
|
||||
console.log("creating account for", email)
|
||||
accountID = await Account.create({
|
||||
email: email!,
|
||||
})
|
||||
}
|
||||
return ctx.subject("account", accountID, { accountID, email })
|
||||
},
|
||||
}).fetch(request, env, ctx)
|
||||
},
|
||||
}
|
|
@ -1,887 +0,0 @@
|
|||
import { z } from "zod"
|
||||
import { Hono, MiddlewareHandler } from "hono"
|
||||
import { cors } from "hono/cors"
|
||||
import { HTTPException } from "hono/http-exception"
|
||||
import { zValidator } from "@hono/zod-validator"
|
||||
import { Resource } from "sst"
|
||||
import { generateText, streamText } from "ai"
|
||||
import { createAnthropic } from "@ai-sdk/anthropic"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { createOpenAICompatible } from "@ai-sdk/openai-compatible"
|
||||
import type { LanguageModelV2Usage, LanguageModelV2Prompt } from "@ai-sdk/provider"
|
||||
import { type ChatCompletionCreateParamsBase } from "openai/resources/chat/completions"
|
||||
import { Actor } from "@opencode/cloud-core/actor.js"
|
||||
import { and, Database, eq, sql } from "@opencode/cloud-core/drizzle/index.js"
|
||||
import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
|
||||
import { KeyTable } from "@opencode/cloud-core/schema/key.sql.js"
|
||||
import { createClient } from "@openauthjs/openauth/client"
|
||||
import { Log } from "@opencode/cloud-core/util/log.js"
|
||||
import { Billing } from "@opencode/cloud-core/billing.js"
|
||||
import { Workspace } from "@opencode/cloud-core/workspace.js"
|
||||
import { BillingTable, PaymentTable, UsageTable } from "@opencode/cloud-core/schema/billing.sql.js"
|
||||
import { centsToMicroCents } from "@opencode/cloud-core/util/price.js"
|
||||
import { Identifier } from "../../core/src/identifier"
|
||||
|
||||
type Env = {}
|
||||
|
||||
let _client: ReturnType<typeof createClient>
|
||||
const client = () => {
|
||||
if (_client) return _client
|
||||
_client = createClient({
|
||||
clientID: "api",
|
||||
issuer: Resource.AUTH_API_URL.value,
|
||||
})
|
||||
return _client
|
||||
}
|
||||
|
||||
const SUPPORTED_MODELS = {
|
||||
"anthropic/claude-sonnet-4": {
|
||||
input: 0.0000015,
|
||||
output: 0.000006,
|
||||
reasoning: 0.0000015,
|
||||
cacheRead: 0.0000001,
|
||||
cacheWrite: 0.0000001,
|
||||
model: () =>
|
||||
createAnthropic({
|
||||
apiKey: Resource.ANTHROPIC_API_KEY.value,
|
||||
})("claude-sonnet-4-20250514"),
|
||||
},
|
||||
"openai/gpt-4.1": {
|
||||
input: 0.0000015,
|
||||
output: 0.000006,
|
||||
reasoning: 0.0000015,
|
||||
cacheRead: 0.0000001,
|
||||
cacheWrite: 0.0000001,
|
||||
model: () =>
|
||||
createOpenAI({
|
||||
apiKey: Resource.OPENAI_API_KEY.value,
|
||||
})("gpt-4.1"),
|
||||
},
|
||||
"zhipuai/glm-4.5-flash": {
|
||||
input: 0,
|
||||
output: 0,
|
||||
reasoning: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
model: () =>
|
||||
createOpenAICompatible({
|
||||
name: "Zhipu AI",
|
||||
baseURL: "https://api.z.ai/api/paas/v4",
|
||||
apiKey: Resource.ZHIPU_API_KEY.value,
|
||||
})("glm-4.5-flash"),
|
||||
},
|
||||
}
|
||||
|
||||
const log = Log.create({
|
||||
namespace: "api",
|
||||
})
|
||||
|
||||
const GatewayAuth: MiddlewareHandler = async (c, next) => {
|
||||
const authHeader = c.req.header("authorization")
|
||||
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return c.json(
|
||||
{
|
||||
error: {
|
||||
message: "Missing API key.",
|
||||
type: "invalid_request_error",
|
||||
param: null,
|
||||
code: "unauthorized",
|
||||
},
|
||||
},
|
||||
401,
|
||||
)
|
||||
}
|
||||
|
||||
const apiKey = authHeader.split(" ")[1]
|
||||
|
||||
// Check against KeyTable
|
||||
const keyRecord = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
id: KeyTable.id,
|
||||
workspaceID: KeyTable.workspaceID,
|
||||
})
|
||||
.from(KeyTable)
|
||||
.where(eq(KeyTable.key, apiKey))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
|
||||
if (!keyRecord) {
|
||||
return c.json(
|
||||
{
|
||||
error: {
|
||||
message: "Invalid API key.",
|
||||
type: "invalid_request_error",
|
||||
param: null,
|
||||
code: "unauthorized",
|
||||
},
|
||||
},
|
||||
401,
|
||||
)
|
||||
}
|
||||
|
||||
c.set("keyRecord", keyRecord)
|
||||
await next()
|
||||
}
|
||||
|
||||
const RestAuth: MiddlewareHandler = async (c, next) => {
|
||||
const authorization = c.req.header("authorization")
|
||||
if (!authorization) {
|
||||
return Actor.provide("public", {}, next)
|
||||
}
|
||||
const token = authorization.split(" ")[1]
|
||||
if (!token)
|
||||
throw new HTTPException(403, {
|
||||
message: "Bearer token is required.",
|
||||
})
|
||||
|
||||
const verified = await client().verify(token)
|
||||
if (verified.err) {
|
||||
throw new HTTPException(403, {
|
||||
message: "Invalid token.",
|
||||
})
|
||||
}
|
||||
let subject = verified.subject as Actor.Info
|
||||
if (subject.type === "account") {
|
||||
const workspaceID = c.req.header("x-opencode-workspace")
|
||||
const email = subject.properties.email
|
||||
if (workspaceID) {
|
||||
const user = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
id: UserTable.id,
|
||||
workspaceID: UserTable.workspaceID,
|
||||
email: UserTable.email,
|
||||
})
|
||||
.from(UserTable)
|
||||
.where(and(eq(UserTable.email, email), eq(UserTable.workspaceID, workspaceID)))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (!user)
|
||||
throw new HTTPException(403, {
|
||||
message: "You do not have access to this workspace.",
|
||||
})
|
||||
subject = {
|
||||
type: "user",
|
||||
properties: {
|
||||
userID: user.id,
|
||||
workspaceID: workspaceID,
|
||||
email: user.email,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
await Actor.provide(subject.type, subject.properties, next)
|
||||
}
|
||||
|
||||
const app = new Hono<{ Bindings: Env; Variables: { keyRecord?: { id: string; workspaceID: string } } }>()
|
||||
.get("/", (c) => c.text("Hello, world!"))
|
||||
.post("/v1/chat/completions", GatewayAuth, async (c) => {
|
||||
try {
|
||||
const body = await c.req.json<ChatCompletionCreateParamsBase>()
|
||||
|
||||
console.log(body)
|
||||
|
||||
const model = SUPPORTED_MODELS[body.model as keyof typeof SUPPORTED_MODELS]?.model()
|
||||
if (!model) throw new Error(`Unsupported model: ${body.model}`)
|
||||
|
||||
const requestBody = transformOpenAIRequestToAiSDK()
|
||||
|
||||
return body.stream ? await handleStream() : await handleGenerate()
|
||||
|
||||
async function handleStream() {
|
||||
const result = await streamText({
|
||||
model,
|
||||
...requestBody,
|
||||
})
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const id = `chatcmpl-${Date.now()}`
|
||||
const created = Math.floor(Date.now() / 1000)
|
||||
|
||||
try {
|
||||
for await (const chunk of result.fullStream) {
|
||||
// TODO
|
||||
//console.log("!!! CHUCK !!!", chunk);
|
||||
switch (chunk.type) {
|
||||
case "text-delta": {
|
||||
const data = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: chunk.text,
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||
break
|
||||
}
|
||||
|
||||
case "reasoning-delta": {
|
||||
const data = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
reasoning_content: chunk.text,
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||
break
|
||||
}
|
||||
|
||||
case "tool-call": {
|
||||
const data = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
tool_calls: [
|
||||
{
|
||||
id: chunk.toolCallId,
|
||||
type: "function",
|
||||
function: {
|
||||
name: chunk.toolName,
|
||||
arguments: JSON.stringify(chunk.input),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||
break
|
||||
}
|
||||
|
||||
case "error": {
|
||||
const data = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model: body.model,
|
||||
error: {
|
||||
message: chunk.error,
|
||||
type: "server_error",
|
||||
},
|
||||
}
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
break
|
||||
}
|
||||
|
||||
case "finish": {
|
||||
const finishReason =
|
||||
{
|
||||
stop: "stop",
|
||||
length: "length",
|
||||
"content-filter": "content_filter",
|
||||
"tool-calls": "tool_calls",
|
||||
error: "stop",
|
||||
other: "stop",
|
||||
unknown: "stop",
|
||||
}[chunk.finishReason] || "stop"
|
||||
|
||||
const data = {
|
||||
id,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {},
|
||||
finish_reason: finishReason,
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: chunk.totalUsage.inputTokens,
|
||||
completion_tokens: chunk.totalUsage.outputTokens,
|
||||
total_tokens: chunk.totalUsage.totalTokens,
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: chunk.totalUsage.reasoningTokens,
|
||||
},
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: chunk.totalUsage.cachedInputTokens,
|
||||
},
|
||||
},
|
||||
}
|
||||
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
|
||||
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
|
||||
controller.close()
|
||||
break
|
||||
}
|
||||
|
||||
//case "stream-start":
|
||||
//case "response-metadata":
|
||||
case "start-step":
|
||||
case "finish-step":
|
||||
case "text-start":
|
||||
case "text-end":
|
||||
case "reasoning-start":
|
||||
case "reasoning-end":
|
||||
case "tool-input-start":
|
||||
case "tool-input-delta":
|
||||
case "tool-input-end":
|
||||
case "raw":
|
||||
default:
|
||||
// Log unknown chunk types for debugging
|
||||
console.warn(`Unknown chunk type: ${(chunk as any).type}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
controller.error(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleGenerate() {
|
||||
const response = await generateText({
|
||||
model,
|
||||
...requestBody,
|
||||
})
|
||||
await trackUsage(body.model, response.usage)
|
||||
return c.json({
|
||||
id: `chatcmpl-${Date.now()}`,
|
||||
object: "chat.completion" as const,
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: body.model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: {
|
||||
role: "assistant" as const,
|
||||
content: response.content?.find((c) => c.type === "text")?.text ?? "",
|
||||
reasoning_content: response.content?.find((c) => c.type === "reasoning")?.text,
|
||||
tool_calls: response.content
|
||||
?.filter((c) => c.type === "tool-call")
|
||||
.map((toolCall) => ({
|
||||
id: toolCall.toolCallId,
|
||||
type: "function" as const,
|
||||
function: {
|
||||
name: toolCall.toolName,
|
||||
arguments: toolCall.input,
|
||||
},
|
||||
})),
|
||||
},
|
||||
finish_reason:
|
||||
(
|
||||
{
|
||||
stop: "stop",
|
||||
length: "length",
|
||||
"content-filter": "content_filter",
|
||||
"tool-calls": "tool_calls",
|
||||
error: "stop",
|
||||
other: "stop",
|
||||
unknown: "stop",
|
||||
} as const
|
||||
)[response.finishReason] || "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: response.usage?.inputTokens,
|
||||
completion_tokens: response.usage?.outputTokens,
|
||||
total_tokens: response.usage?.totalTokens,
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: response.usage?.reasoningTokens,
|
||||
},
|
||||
prompt_tokens_details: {
|
||||
cached_tokens: response.usage?.cachedInputTokens,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function transformOpenAIRequestToAiSDK() {
|
||||
const prompt = transformMessages()
|
||||
|
||||
return {
|
||||
prompt,
|
||||
maxOutputTokens: body.max_tokens ?? body.max_completion_tokens ?? undefined,
|
||||
temperature: body.temperature ?? undefined,
|
||||
topP: body.top_p ?? undefined,
|
||||
frequencyPenalty: body.frequency_penalty ?? undefined,
|
||||
presencePenalty: body.presence_penalty ?? undefined,
|
||||
providerOptions: body.reasoning_effort
|
||||
? {
|
||||
anthropic: {
|
||||
reasoningEffort: body.reasoning_effort,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
stopSequences: (typeof body.stop === "string" ? [body.stop] : body.stop) ?? undefined,
|
||||
responseFormat: (() => {
|
||||
if (!body.response_format) return { type: "text" }
|
||||
if (body.response_format.type === "json_schema")
|
||||
return {
|
||||
type: "json",
|
||||
schema: body.response_format.json_schema.schema,
|
||||
name: body.response_format.json_schema.name,
|
||||
description: body.response_format.json_schema.description,
|
||||
}
|
||||
if (body.response_format.type === "json_object") return { type: "json" }
|
||||
throw new Error("Unsupported response format")
|
||||
})(),
|
||||
seed: body.seed ?? undefined,
|
||||
}
|
||||
|
||||
function transformTools() {
|
||||
const { tools, tool_choice } = body
|
||||
|
||||
if (!tools || tools.length === 0) {
|
||||
return { tools: undefined, toolChoice: undefined }
|
||||
}
|
||||
|
||||
const aiSdkTools = tools.reduce(
|
||||
(acc, tool) => {
|
||||
acc[tool.function.name] = {
|
||||
type: "function" as const,
|
||||
name: tool.function.name,
|
||||
description: tool.function.description,
|
||||
inputSchema: tool.function.parameters,
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
)
|
||||
|
||||
let aiSdkToolChoice
|
||||
if (tool_choice == null) {
|
||||
aiSdkToolChoice = undefined
|
||||
} else if (tool_choice === "auto") {
|
||||
aiSdkToolChoice = "auto"
|
||||
} else if (tool_choice === "none") {
|
||||
aiSdkToolChoice = "none"
|
||||
} else if (tool_choice === "required") {
|
||||
aiSdkToolChoice = "required"
|
||||
} else if (tool_choice.type === "function") {
|
||||
aiSdkToolChoice = {
|
||||
type: "tool",
|
||||
toolName: tool_choice.function.name,
|
||||
}
|
||||
}
|
||||
|
||||
return { tools: aiSdkTools, toolChoice: aiSdkToolChoice }
|
||||
}
|
||||
|
||||
function transformMessages() {
|
||||
const { messages } = body
|
||||
const prompt: LanguageModelV2Prompt = []
|
||||
|
||||
for (const message of messages) {
|
||||
switch (message.role) {
|
||||
case "system": {
|
||||
prompt.push({
|
||||
role: "system",
|
||||
content: message.content as string,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "user": {
|
||||
if (typeof message.content === "string") {
|
||||
prompt.push({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: message.content }],
|
||||
})
|
||||
} else {
|
||||
const content = message.content.map((part) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return { type: "text" as const, text: part.text }
|
||||
case "image_url":
|
||||
return {
|
||||
type: "file" as const,
|
||||
mediaType: "image/jpeg" as const,
|
||||
data: part.image_url.url,
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported content part type: ${(part as any).type}`)
|
||||
}
|
||||
})
|
||||
prompt.push({
|
||||
role: "user",
|
||||
content,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "assistant": {
|
||||
const content: Array<
|
||||
| { type: "text"; text: string }
|
||||
| {
|
||||
type: "tool-call"
|
||||
toolCallId: string
|
||||
toolName: string
|
||||
input: any
|
||||
}
|
||||
> = []
|
||||
|
||||
if (message.content) {
|
||||
content.push({
|
||||
type: "text",
|
||||
text: message.content as string,
|
||||
})
|
||||
}
|
||||
|
||||
if (message.tool_calls) {
|
||||
for (const toolCall of message.tool_calls) {
|
||||
content.push({
|
||||
type: "tool-call",
|
||||
toolCallId: toolCall.id,
|
||||
toolName: toolCall.function.name,
|
||||
input: JSON.parse(toolCall.function.arguments),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
prompt.push({
|
||||
role: "assistant",
|
||||
content,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case "tool": {
|
||||
prompt.push({
|
||||
role: "tool",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolName: "placeholder",
|
||||
toolCallId: message.tool_call_id,
|
||||
output: {
|
||||
type: "text",
|
||||
value: message.content as string,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Unsupported message role: ${message.role}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return prompt
|
||||
}
|
||||
}
|
||||
|
||||
async function trackUsage(model: string, usage: LanguageModelV2Usage) {
|
||||
const keyRecord = c.get("keyRecord")
|
||||
if (!keyRecord) return
|
||||
|
||||
const modelData = SUPPORTED_MODELS[model as keyof typeof SUPPORTED_MODELS]
|
||||
if (!modelData) throw new Error(`Unsupported model: ${model}`)
|
||||
|
||||
const inputCost = modelData.input * (usage.inputTokens ?? 0)
|
||||
const outputCost = modelData.output * (usage.outputTokens ?? 0)
|
||||
const reasoningCost = modelData.reasoning * (usage.reasoningTokens ?? 0)
|
||||
const cacheReadCost = modelData.cacheRead * (usage.cachedInputTokens ?? 0)
|
||||
const cacheWriteCost = modelData.cacheWrite * (usage.outputTokens ?? 0)
|
||||
|
||||
const totalCost = inputCost + outputCost + reasoningCost + cacheReadCost + cacheWriteCost
|
||||
|
||||
await Actor.provide("system", { workspaceID: keyRecord.workspaceID }, async () => {
|
||||
await Billing.consume({
|
||||
model,
|
||||
inputTokens: usage.inputTokens ?? 0,
|
||||
outputTokens: usage.outputTokens ?? 0,
|
||||
reasoningTokens: usage.reasoningTokens ?? 0,
|
||||
cacheReadTokens: usage.cachedInputTokens ?? 0,
|
||||
cacheWriteTokens: usage.outputTokens ?? 0,
|
||||
costInCents: totalCost * 100,
|
||||
})
|
||||
})
|
||||
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(KeyTable)
|
||||
.set({ timeUsed: sql`now()` })
|
||||
.where(eq(KeyTable.id, keyRecord.id)),
|
||||
)
|
||||
}
|
||||
} catch (error: any) {
|
||||
return c.json({ error: { message: error.message } }, 500)
|
||||
}
|
||||
})
|
||||
.use("/*", cors())
|
||||
.use(RestAuth)
|
||||
.get("/rest/account", async (c) => {
|
||||
const account = Actor.assert("account")
|
||||
let workspaces = await Workspace.list()
|
||||
if (workspaces.length === 0) {
|
||||
await Workspace.create()
|
||||
workspaces = await Workspace.list()
|
||||
}
|
||||
return c.json({
|
||||
id: account.properties.accountID,
|
||||
email: account.properties.email,
|
||||
workspaces,
|
||||
})
|
||||
})
|
||||
.get("/billing/info", async (c) => {
|
||||
const billing = await Billing.get()
|
||||
const payments = await Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(PaymentTable)
|
||||
.where(eq(PaymentTable.workspaceID, Actor.workspace()))
|
||||
.orderBy(sql`${PaymentTable.timeCreated} DESC`)
|
||||
.limit(100),
|
||||
)
|
||||
const usage = await Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(UsageTable)
|
||||
.where(eq(UsageTable.workspaceID, Actor.workspace()))
|
||||
.orderBy(sql`${UsageTable.timeCreated} DESC`)
|
||||
.limit(100),
|
||||
)
|
||||
return c.json({ billing, payments, usage })
|
||||
})
|
||||
.post(
|
||||
"/billing/checkout",
|
||||
zValidator(
|
||||
"json",
|
||||
z.custom<{
|
||||
success_url: string
|
||||
cancel_url: string
|
||||
}>(),
|
||||
),
|
||||
async (c) => {
|
||||
const account = Actor.assert("user")
|
||||
|
||||
const body = await c.req.json()
|
||||
|
||||
const customer = await Billing.get()
|
||||
const session = await Billing.stripe().checkout.sessions.create({
|
||||
mode: "payment",
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: "usd",
|
||||
product_data: {
|
||||
name: "OpenControl credits",
|
||||
},
|
||||
unit_amount: 2000, // $20 minimum
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
payment_intent_data: {
|
||||
setup_future_usage: "on_session",
|
||||
},
|
||||
...(customer.customerID
|
||||
? { customer: customer.customerID }
|
||||
: {
|
||||
customer_email: account.properties.email,
|
||||
customer_creation: "always",
|
||||
}),
|
||||
metadata: {
|
||||
workspaceID: Actor.workspace(),
|
||||
},
|
||||
currency: "usd",
|
||||
payment_method_types: ["card"],
|
||||
success_url: body.success_url,
|
||||
cancel_url: body.cancel_url,
|
||||
})
|
||||
|
||||
return c.json({
|
||||
url: session.url,
|
||||
})
|
||||
},
|
||||
)
|
||||
.post("/billing/portal", async (c) => {
|
||||
const body = await c.req.json()
|
||||
|
||||
const customer = await Billing.get()
|
||||
if (!customer?.customerID) {
|
||||
throw new Error("No stripe customer ID")
|
||||
}
|
||||
|
||||
const session = await Billing.stripe().billingPortal.sessions.create({
|
||||
customer: customer.customerID,
|
||||
return_url: body.return_url,
|
||||
})
|
||||
|
||||
return c.json({
|
||||
url: session.url,
|
||||
})
|
||||
})
|
||||
.post("/stripe/webhook", async (c) => {
|
||||
const body = await Billing.stripe().webhooks.constructEventAsync(
|
||||
await c.req.text(),
|
||||
c.req.header("stripe-signature")!,
|
||||
Resource.STRIPE_WEBHOOK_SECRET.value,
|
||||
)
|
||||
|
||||
console.log(body.type, JSON.stringify(body, null, 2))
|
||||
if (body.type === "checkout.session.completed") {
|
||||
const workspaceID = body.data.object.metadata?.workspaceID
|
||||
const customerID = body.data.object.customer as string
|
||||
const paymentID = body.data.object.payment_intent as string
|
||||
const amount = body.data.object.amount_total
|
||||
|
||||
if (!workspaceID) throw new Error("Workspace ID not found")
|
||||
if (!customerID) throw new Error("Customer ID not found")
|
||||
if (!amount) throw new Error("Amount not found")
|
||||
if (!paymentID) throw new Error("Payment ID not found")
|
||||
|
||||
await Actor.provide("system", { workspaceID }, async () => {
|
||||
const customer = await Billing.get()
|
||||
if (customer?.customerID && customer.customerID !== customerID) throw new Error("Customer ID mismatch")
|
||||
|
||||
// set customer metadata
|
||||
if (!customer?.customerID) {
|
||||
await Billing.stripe().customers.update(customerID, {
|
||||
metadata: {
|
||||
workspaceID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// get payment method for the payment intent
|
||||
const paymentIntent = await Billing.stripe().paymentIntents.retrieve(paymentID, {
|
||||
expand: ["payment_method"],
|
||||
})
|
||||
const paymentMethod = paymentIntent.payment_method
|
||||
if (!paymentMethod || typeof paymentMethod === "string") throw new Error("Payment method not expanded")
|
||||
|
||||
await Database.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
balance: sql`${BillingTable.balance} + ${centsToMicroCents(amount)}`,
|
||||
customerID,
|
||||
paymentMethodID: paymentMethod.id,
|
||||
paymentMethodLast4: paymentMethod.card!.last4,
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
await tx.insert(PaymentTable).values({
|
||||
workspaceID,
|
||||
id: Identifier.create("payment"),
|
||||
amount: centsToMicroCents(amount),
|
||||
paymentID,
|
||||
customerID,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
console.log("finished handling")
|
||||
|
||||
return c.json("ok", 200)
|
||||
})
|
||||
.get("/keys", async (c) => {
|
||||
const user = Actor.assert("user")
|
||||
|
||||
const keys = await Database.use((tx) =>
|
||||
tx
|
||||
.select({
|
||||
id: KeyTable.id,
|
||||
name: KeyTable.name,
|
||||
key: KeyTable.key,
|
||||
userID: KeyTable.userID,
|
||||
timeCreated: KeyTable.timeCreated,
|
||||
timeUsed: KeyTable.timeUsed,
|
||||
})
|
||||
.from(KeyTable)
|
||||
.where(eq(KeyTable.workspaceID, user.properties.workspaceID))
|
||||
.orderBy(sql`${KeyTable.timeCreated} DESC`),
|
||||
)
|
||||
|
||||
return c.json({ keys })
|
||||
})
|
||||
.post("/keys", zValidator("json", z.object({ name: z.string().min(1).max(255) })), async (c) => {
|
||||
const user = Actor.assert("user")
|
||||
const { name } = c.req.valid("json")
|
||||
|
||||
// Generate secret key: sk- + 64 random characters (upper, lower, numbers)
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
let randomPart = ""
|
||||
for (let i = 0; i < 64; i++) {
|
||||
randomPart += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
const secretKey = `sk-${randomPart}`
|
||||
|
||||
const keyRecord = await Database.use((tx) =>
|
||||
tx
|
||||
.insert(KeyTable)
|
||||
.values({
|
||||
id: Identifier.create("key"),
|
||||
workspaceID: user.properties.workspaceID,
|
||||
userID: user.properties.userID,
|
||||
name,
|
||||
key: secretKey,
|
||||
timeUsed: null,
|
||||
})
|
||||
.returning(),
|
||||
)
|
||||
|
||||
return c.json({
|
||||
key: secretKey,
|
||||
id: keyRecord[0].id,
|
||||
name: keyRecord[0].name,
|
||||
created: keyRecord[0].timeCreated,
|
||||
})
|
||||
})
|
||||
.delete("/keys/:id", async (c) => {
|
||||
const user = Actor.assert("user")
|
||||
const keyId = c.req.param("id")
|
||||
|
||||
const result = await Database.use((tx) =>
|
||||
tx
|
||||
.delete(KeyTable)
|
||||
.where(and(eq(KeyTable.id, keyId), eq(KeyTable.workspaceID, user.properties.workspaceID)))
|
||||
.returning({ id: KeyTable.id }),
|
||||
)
|
||||
|
||||
if (result.length === 0) {
|
||||
return c.json({ error: "Key not found" }, 404)
|
||||
}
|
||||
|
||||
return c.json({ success: true, id: result[0].id })
|
||||
})
|
||||
.all("*", (c) => c.text("Not Found"))
|
||||
|
||||
export type ApiType = typeof app
|
||||
|
||||
export default app
|
92
cloud/function/sst-env.d.ts
vendored
92
cloud/function/sst-env.d.ts
vendored
|
@ -1,92 +0,0 @@
|
|||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
|
||||
import "sst"
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"ANTHROPIC_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"AUTH_API_URL": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Console": {
|
||||
"type": "sst.cloudflare.StaticSite"
|
||||
"url": string
|
||||
}
|
||||
"DATABASE_PASSWORD": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"DATABASE_USERNAME": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Database": {
|
||||
"database": string
|
||||
"host": string
|
||||
"password": string
|
||||
"port": number
|
||||
"type": "sst.sst.Linkable"
|
||||
"username": string
|
||||
}
|
||||
"GITHUB_APP_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"GITHUB_APP_PRIVATE_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"GITHUB_CLIENT_ID_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"GITHUB_CLIENT_SECRET_CONSOLE": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"GOOGLE_CLIENT_ID": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"OPENAI_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"STRIPE_SECRET_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"STRIPE_WEBHOOK_SECRET": {
|
||||
"type": "sst.sst.Linkable"
|
||||
"value": string
|
||||
}
|
||||
"Web": {
|
||||
"type": "sst.cloudflare.Astro"
|
||||
"url": string
|
||||
}
|
||||
"ZHIPU_API_KEY": {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
}
|
||||
}
|
||||
// cloudflare
|
||||
import * as cloudflare from "@cloudflare/workers-types";
|
||||
declare module "sst" {
|
||||
export interface Resource {
|
||||
"Api": cloudflare.Service
|
||||
"AuthApi": cloudflare.Service
|
||||
"AuthStorage": cloudflare.KVNamespace
|
||||
"Bucket": cloudflare.R2Bucket
|
||||
"GatewayApi": cloudflare.Service
|
||||
}
|
||||
}
|
||||
|
||||
import "sst"
|
||||
export {}
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["@cloudflare/workers-types", "node"]
|
||||
}
|
||||
}
|
2
cloud/web/.gitignore
vendored
2
cloud/web/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
node_modules
|
||||
dist
|
|
@ -1,38 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-color-mode="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OpenControl</title>
|
||||
<link rel="shortcut icon" type="image/ico" href="/favicon.ico" />
|
||||
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
||||
<link rel="icon" href="/favicon.svg" media="(prefers-color-scheme: light)">
|
||||
<link rel="icon" href="/favicon-dark.svg" media="(prefers-color-scheme: dark)">
|
||||
<link rel="shortcut icon" href="/favicon.svg" type="image/svg+xml">
|
||||
|
||||
<meta property="twitter:image" content="%BASE_URL%/social-share.png">
|
||||
<meta property="og:title" content="OpenControl">
|
||||
<meta property="og:url" content="%BASE_URL%">
|
||||
<meta property="og:locale" content="en">
|
||||
<meta property="og:description" content="Control your infrastructure with AI.">
|
||||
<meta property="og:site_name" content="OpenControl">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="description" content="Control your infrastructure with AI.">
|
||||
<meta property="og:image" content="%BASE_URL%/social-share.png">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=Rubik:wght@300..900&display=swap" rel="stylesheet">
|
||||
<!--ssr-head-->
|
||||
<!--ssr-assets-->
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
<!--ssr-outlet-->
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/entry-client.tsx"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,29 +0,0 @@
|
|||
0 info it worked if it ends with ok
|
||||
1 verbose cli [
|
||||
1 verbose cli '/usr/local/bin/node',
|
||||
1 verbose cli '/Users/frank/Sites/opencode/node_modules/.bin/npm',
|
||||
1 verbose cli 'run',
|
||||
1 verbose cli 'dev'
|
||||
1 verbose cli ]
|
||||
2 info using npm@2.15.12
|
||||
3 info using node@v20.18.1
|
||||
4 verbose stack Error: Invalid name: "@opencode/cloud/web"
|
||||
4 verbose stack at ensureValidName (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/fixer.js:336:15)
|
||||
4 verbose stack at Object.fixNameField (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/fixer.js:215:5)
|
||||
4 verbose stack at /Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/normalize.js:32:38
|
||||
4 verbose stack at Array.forEach (<anonymous>)
|
||||
4 verbose stack at normalize (/Users/frank/Sites/opencode/node_modules/npm/node_modules/normalize-package-data/lib/normalize.js:31:15)
|
||||
4 verbose stack at final (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:349:5)
|
||||
4 verbose stack at then (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:124:5)
|
||||
4 verbose stack at ReadFileContext.<anonymous> (/Users/frank/Sites/opencode/node_modules/npm/node_modules/read-package-json/read-json.js:295:20)
|
||||
4 verbose stack at ReadFileContext.callback (/Users/frank/Sites/opencode/node_modules/npm/node_modules/graceful-fs/graceful-fs.js:78:16)
|
||||
4 verbose stack at FSReqCallback.readFileAfterOpen [as oncomplete] (node:fs:299:13)
|
||||
5 verbose cwd /Users/frank/Sites/opencode/cloud/web
|
||||
6 error Darwin 24.5.0
|
||||
7 error argv "/usr/local/bin/node" "/Users/frank/Sites/opencode/node_modules/.bin/npm" "run" "dev"
|
||||
8 error node v20.18.1
|
||||
9 error npm v2.15.12
|
||||
10 error Invalid name: "@opencode/cloud/web"
|
||||
11 error If you need help, you may report this error at:
|
||||
11 error <https://github.com/npm/npm/issues>
|
||||
12 verbose exit [ 1, true ]
|
|
@ -1,32 +0,0 @@
|
|||
{
|
||||
"name": "@opencode/cloud-web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "bun build:server && bun build:client",
|
||||
"build:client": "vite build --outDir dist/client",
|
||||
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
|
||||
"serve": "vite preview",
|
||||
"sst:dev": "bun sst shell --target Console -- bun dev"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:",
|
||||
"vite": "6.2.2",
|
||||
"vite-plugin-pages": "0.32.5",
|
||||
"vite-plugin-solid": "2.11.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kobalte/core": "0.13.9",
|
||||
"@openauthjs/solid": "0.0.0-20250322224806",
|
||||
"@solid-primitives/storage": "4.3.1",
|
||||
"@solidjs/meta": "0.29.4",
|
||||
"@solidjs/router": "0.15.3",
|
||||
"solid-js": "1.9.5",
|
||||
"solid-list": "0.3.0"
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 168 KiB |
|
@ -1,3 +0,0 @@
|
|||
<svg width="28" height="32" viewBox="0 0 28 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 31.5L0 23.6873V7.81266L14 0L28 7.81266V23.6873L14 31.5ZM14 28.4664L25.3456 22.0251V9.47493L14 2.99209L2.65443 9.47493V22.0251L14 28.4664ZM13.9572 24.6016C12.2732 24.6016 10.7176 24.1999 9.29052 23.3964C7.89195 22.593 6.7788 21.5125 5.95107 20.155C5.12334 18.7698 4.70948 17.2599 4.70948 15.6253C4.70948 13.9908 5.12334 12.4947 5.95107 11.1372C6.7788 9.77968 7.89195 8.69921 9.29052 7.89578C10.7176 7.06464 12.2732 6.64908 13.9572 6.64908C15.6412 6.64908 17.1825 7.06464 18.581 7.89578C19.9796 8.69921 21.0928 9.77968 21.9205 11.1372C22.7768 12.4947 23.2049 13.9908 23.2049 15.6253C23.2049 17.2599 22.791 18.7559 21.9633 20.1135C21.1356 21.471 20.0224 22.5653 18.6239 23.3964C17.2253 24.1999 15.6697 24.6016 13.9572 24.6016ZM13.9572 22.2744C15.213 22.2744 16.3547 21.9697 17.3823 21.3602C18.4098 20.7507 19.2375 19.9472 19.8654 18.9499C20.4934 17.9248 20.8073 16.8166 20.8073 15.6253C20.8073 14.4063 20.4934 13.2982 19.8654 12.3008C19.2375 11.3034 18.4098 10.5 17.3823 9.8905C16.3547 9.281 15.213 8.97625 13.9572 8.97625C12.7299 8.97625 11.5882 9.281 10.5321 9.8905C9.50459 10.5 8.67686 11.3034 8.04893 12.3008C7.421 13.2982 7.10703 14.4063 7.10703 15.6253C7.10703 16.8166 7.421 17.9248 8.04893 18.9499C8.67686 19.9472 9.50459 20.7507 10.5321 21.3602C11.5882 21.9697 12.7299 22.2744 13.9572 22.2744Z" fill="black"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
|
@ -1,24 +0,0 @@
|
|||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { generateHydrationScript, getAssets } from "solid-js/web"
|
||||
|
||||
const dist = import.meta.resolve("../dist").replace("file://", "")
|
||||
const serverEntry = await import("../dist/server/entry-server.js")
|
||||
const template = fs.readFileSync(path.join(dist, "client/index.html"), "utf-8")
|
||||
fs.writeFileSync(path.join(dist, "client/fallback.html"), template)
|
||||
|
||||
const routes = ["/", "/foo"]
|
||||
for (const route of routes) {
|
||||
const { app } = serverEntry.render({ url: route })
|
||||
const html = template
|
||||
.replace("<!--ssr-outlet-->", app)
|
||||
.replace("<!--ssr-head-->", generateHydrationScript())
|
||||
.replace("<!--ssr-assets-->", getAssets())
|
||||
const filePath = dist + `/client${route === "/" ? "/index" : route}.html`
|
||||
fs.mkdirSync(path.dirname(filePath), {
|
||||
recursive: true,
|
||||
})
|
||||
fs.writeFileSync(filePath, html)
|
||||
|
||||
console.log(`Pre-rendered: ${filePath}`)
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
/// <reference types="vite-plugin-pages/client-solid" />
|
||||
|
||||
import { Router } from "@solidjs/router"
|
||||
import routes from "~solid-pages"
|
||||
import "./ui/style/index.css"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { AccountProvider } from "./components/context-account"
|
||||
import { DialogProvider } from "./ui/context-dialog"
|
||||
import { DialogString } from "./ui/dialog-string"
|
||||
import { DialogSelect } from "./ui/dialog-select"
|
||||
import { ThemeProvider } from "./components/context-theme"
|
||||
import { Suspense } from "solid-js"
|
||||
import { OpenAuthProvider } from "./components/context-openauth"
|
||||
|
||||
export function App(props: { url?: string }) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<Suspense>
|
||||
<DialogProvider>
|
||||
<DialogString />
|
||||
<DialogSelect />
|
||||
<OpenAuthProvider
|
||||
clientID="web"
|
||||
issuer={import.meta.env.VITE_AUTH_URL || "http://dummy"}
|
||||
>
|
||||
<AccountProvider>
|
||||
<MetaProvider>
|
||||
<Router
|
||||
children={routes}
|
||||
url={props.url}
|
||||
root={(props) => {
|
||||
return <>{props.children}</>
|
||||
}}
|
||||
/>
|
||||
</MetaProvider>
|
||||
</AccountProvider>
|
||||
</OpenAuthProvider>
|
||||
</DialogProvider>
|
||||
</Suspense>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 439 KiB |
|
@ -1,99 +0,0 @@
|
|||
import { createContext, createEffect, ParentProps, Suspense, useContext } from "solid-js"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useOpenAuth } from "./context-openauth"
|
||||
import { createAsync } from "@solidjs/router"
|
||||
import { isServer } from "solid-js/web"
|
||||
|
||||
type Storage = {
|
||||
accounts: Record<
|
||||
string,
|
||||
{
|
||||
id: string
|
||||
email: string
|
||||
workspaces: {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
}[]
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
const context = createContext<ReturnType<typeof init>>()
|
||||
|
||||
function init() {
|
||||
const auth = useOpenAuth()
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<Storage>({
|
||||
accounts: {},
|
||||
}),
|
||||
{
|
||||
name: "opencontrol.account",
|
||||
},
|
||||
)
|
||||
|
||||
async function refresh(id: string) {
|
||||
return fetch(import.meta.env.VITE_API_URL + "/rest/account", {
|
||||
headers: {
|
||||
authorization: `Bearer ${await auth.access(id)}`,
|
||||
},
|
||||
})
|
||||
.then((val) => val.json())
|
||||
.then((val) => setStore("accounts", id, val as any))
|
||||
}
|
||||
|
||||
createEffect((previous: string[]) => {
|
||||
if (Object.keys(auth.all).length === 0) {
|
||||
return []
|
||||
}
|
||||
for (const item of Object.values(auth.all)) {
|
||||
if (previous.includes(item.id)) continue
|
||||
refresh(item.id)
|
||||
}
|
||||
return Object.keys(auth.all)
|
||||
}, [] as string[])
|
||||
|
||||
const result = {
|
||||
get all() {
|
||||
return Object.keys(auth.all)
|
||||
.map((id) => store.accounts[id])
|
||||
.filter(Boolean)
|
||||
},
|
||||
get current() {
|
||||
if (!auth.subject) return undefined
|
||||
return store.accounts[auth.subject.id]
|
||||
},
|
||||
refresh,
|
||||
get ready() {
|
||||
return Object.keys(auth.all).length === result.all.length
|
||||
},
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function AccountProvider(props: ParentProps) {
|
||||
const ctx = init()
|
||||
const resource = createAsync(async () => {
|
||||
await new Promise<void>((resolve) => {
|
||||
if (isServer) return resolve()
|
||||
createEffect(() => {
|
||||
if (ctx.ready) resolve()
|
||||
})
|
||||
})
|
||||
return null
|
||||
})
|
||||
return (
|
||||
<Suspense>
|
||||
{resource()}
|
||||
<context.Provider value={ctx}>{props.children}</context.Provider>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAccount() {
|
||||
const result = useContext(context)
|
||||
if (!result) throw new Error("no account context")
|
||||
return result
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
import { createClient } from "@openauthjs/openauth/client"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { createAsync } from "@solidjs/router"
|
||||
import {
|
||||
batch,
|
||||
createContext,
|
||||
createEffect,
|
||||
createResource,
|
||||
createSignal,
|
||||
onMount,
|
||||
ParentProps,
|
||||
Show,
|
||||
Suspense,
|
||||
useContext,
|
||||
} from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { isServer } from "solid-js/web"
|
||||
|
||||
interface Storage {
|
||||
subjects: Record<string, SubjectInfo>
|
||||
current?: string
|
||||
}
|
||||
|
||||
interface Context {
|
||||
all: Record<string, SubjectInfo>
|
||||
subject?: SubjectInfo
|
||||
switch(id: string): void
|
||||
logout(id: string): void
|
||||
access(id?: string): Promise<string | undefined>
|
||||
authorize(opts?: AuthorizeOptions): void
|
||||
}
|
||||
|
||||
export interface AuthorizeOptions {
|
||||
redirectPath?: string
|
||||
provider?: string
|
||||
}
|
||||
|
||||
interface SubjectInfo {
|
||||
id: string
|
||||
refresh: string
|
||||
}
|
||||
|
||||
interface AuthContextOpts {
|
||||
issuer: string
|
||||
clientID: string
|
||||
}
|
||||
|
||||
const context = createContext<Context>()
|
||||
|
||||
export function OpenAuthProvider(props: ParentProps<AuthContextOpts>) {
|
||||
const client = createClient({
|
||||
issuer: props.issuer,
|
||||
clientID: props.clientID,
|
||||
})
|
||||
const [storage, setStorage] = makePersisted(
|
||||
createStore<Storage>({
|
||||
subjects: {},
|
||||
}),
|
||||
{
|
||||
name: `${props.issuer}.auth`,
|
||||
},
|
||||
)
|
||||
|
||||
const resource = createAsync(async () => {
|
||||
if (isServer) return true
|
||||
const hash = new URLSearchParams(window.location.search.substring(1))
|
||||
const code = hash.get("code")
|
||||
const state = hash.get("state")
|
||||
if (code && state) {
|
||||
const oldState = sessionStorage.getItem("openauth.state")
|
||||
const verifier = sessionStorage.getItem("openauth.verifier")
|
||||
const redirect = sessionStorage.getItem("openauth.redirect")
|
||||
if (redirect && verifier && oldState === state) {
|
||||
const result = await client.exchange(code, redirect, verifier)
|
||||
if (!result.err) {
|
||||
const id = result.tokens.refresh.split(":").slice(0, -1).join(":")
|
||||
batch(() => {
|
||||
setStorage("subjects", id, {
|
||||
id: id,
|
||||
refresh: result.tokens.refresh,
|
||||
})
|
||||
setStorage("current", id)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
async function authorize(opts?: AuthorizeOptions) {
|
||||
const redirect = new URL(window.location.origin + (opts?.redirectPath ?? "/")).toString()
|
||||
const authorize = await client.authorize(redirect, "code", {
|
||||
pkce: true,
|
||||
provider: opts?.provider,
|
||||
})
|
||||
sessionStorage.setItem("openauth.state", authorize.challenge.state)
|
||||
sessionStorage.setItem("openauth.redirect", redirect)
|
||||
if (authorize.challenge.verifier) sessionStorage.setItem("openauth.verifier", authorize.challenge.verifier)
|
||||
window.location.href = authorize.url
|
||||
}
|
||||
|
||||
const accessCache = new Map<string, string>()
|
||||
const pendingRequests = new Map<string, Promise<any>>()
|
||||
async function access(id: string) {
|
||||
const pending = pendingRequests.get(id)
|
||||
if (pending) return pending
|
||||
const promise = (async () => {
|
||||
const existing = accessCache.get(id)
|
||||
const subject = storage.subjects[id]
|
||||
const access = await client.refresh(subject.refresh, {
|
||||
access: existing,
|
||||
})
|
||||
if (access.err) {
|
||||
pendingRequests.delete(id)
|
||||
ctx.logout(id)
|
||||
return
|
||||
}
|
||||
if (access.tokens) {
|
||||
setStorage("subjects", id, "refresh", access.tokens.refresh)
|
||||
accessCache.set(id, access.tokens.access)
|
||||
}
|
||||
pendingRequests.delete(id)
|
||||
return access.tokens?.access || existing!
|
||||
})()
|
||||
pendingRequests.set(id, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
const ctx: Context = {
|
||||
get all() {
|
||||
return storage.subjects
|
||||
},
|
||||
get subject() {
|
||||
if (!storage.current) return
|
||||
return storage.subjects[storage.current!]
|
||||
},
|
||||
switch(id: string) {
|
||||
if (!storage.subjects[id]) return
|
||||
setStorage("current", id)
|
||||
},
|
||||
authorize,
|
||||
logout(id: string) {
|
||||
if (!storage.subjects[id]) return
|
||||
setStorage(
|
||||
produce((s) => {
|
||||
delete s.subjects[id]
|
||||
if (s.current === id) s.current = Object.keys(s.subjects)[0]
|
||||
}),
|
||||
)
|
||||
},
|
||||
async access(id?: string) {
|
||||
id = id || storage.current
|
||||
if (!id) return
|
||||
return access(id || storage.current!)
|
||||
},
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!resource()) return
|
||||
if (storage.current) return
|
||||
const [first] = Object.keys(storage.subjects)
|
||||
if (first) {
|
||||
setStorage("current", first)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{resource()}
|
||||
<context.Provider value={ctx}>{props.children}</context.Provider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function useOpenAuth() {
|
||||
const result = useContext(context)
|
||||
if (!result) throw new Error("no auth context")
|
||||
return result
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import { createStore } from "solid-js/store"
|
||||
import { makePersisted } from "@solid-primitives/storage"
|
||||
import { createEffect } from "solid-js"
|
||||
import { createInitializedContext } from "../util/context"
|
||||
import { isServer } from "solid-js/web"
|
||||
|
||||
interface Storage {
|
||||
mode: "light" | "dark"
|
||||
}
|
||||
|
||||
export const { provider: ThemeProvider, use: useTheme } =
|
||||
createInitializedContext("ThemeContext", () => {
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<Storage>({
|
||||
mode:
|
||||
!isServer &&
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light",
|
||||
}),
|
||||
{
|
||||
name: "theme",
|
||||
},
|
||||
)
|
||||
createEffect(() => {
|
||||
document.documentElement.setAttribute("data-color-mode", store.mode)
|
||||
})
|
||||
|
||||
return {
|
||||
setMode(mode: Storage["mode"]) {
|
||||
setStore("mode", mode)
|
||||
},
|
||||
get mode() {
|
||||
return store.mode
|
||||
},
|
||||
ready: true,
|
||||
}
|
||||
})
|
|
@ -1,13 +0,0 @@
|
|||
/* @refresh reload */
|
||||
|
||||
import { hydrate, render } from "solid-js/web"
|
||||
import { App } from "./app"
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
render(() => <App />, document.getElementById("root")!)
|
||||
}
|
||||
|
||||
if (!import.meta.env.DEV) {
|
||||
if ("_$HY" in window) hydrate(() => <App />, document.getElementById("root")!)
|
||||
else render(() => <App />, document.getElementById("root")!)
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { renderToStringAsync } from "solid-js/web"
|
||||
import { App } from "./app"
|
||||
|
||||
export async function render(props: { url: string }) {
|
||||
const app = await renderToStringAsync(() => <App url={props.url} />)
|
||||
return { app }
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { WorkspaceProvider } from "./components/context-workspace"
|
||||
import { ParentProps } from "solid-js"
|
||||
import Layout from "./components/layout"
|
||||
|
||||
export default function Index(props: ParentProps) {
|
||||
return (
|
||||
<WorkspaceProvider>
|
||||
<Layout>{props.children}</Layout>
|
||||
</WorkspaceProvider>
|
||||
)
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-7) var(--space-5) var(--space-5);
|
||||
|
||||
[data-slot="billing-info"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
[data-slot="header"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1-5);
|
||||
|
||||
h2 {
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03125rem;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text-dimmed);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="balance"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
padding: var(--space-6);
|
||||
border: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@media (min-width: 40rem) {
|
||||
[data-slot="balance"] {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
[data-slot="amount"] {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
import { Button } from "../../ui/button"
|
||||
import { useApi } from "../components/context-api"
|
||||
import { createEffect, createSignal, createResource, For } from "solid-js"
|
||||
import { useWorkspace } from "../components/context-workspace"
|
||||
import style from "./billing.module.css"
|
||||
|
||||
export default function Billing() {
|
||||
const api = useApi()
|
||||
const workspace = useWorkspace()
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
const [billingData] = createResource(async () => {
|
||||
const response = await api.billing.info.$get()
|
||||
return response.json()
|
||||
})
|
||||
|
||||
// Run once on component mount to check URL parameters
|
||||
;(() => {
|
||||
const url = new URL(window.location.href)
|
||||
const result = url.hash
|
||||
|
||||
console.log("STRIPE RESULT", result)
|
||||
|
||||
if (url.hash === "#success") {
|
||||
setIsLoading(true)
|
||||
// Remove the hash from the URL
|
||||
window.history.replaceState(null, "", window.location.pathname + window.location.search)
|
||||
}
|
||||
})()
|
||||
|
||||
createEffect((old?: number) => {
|
||||
if (old && old !== billingData()?.billing?.balance) {
|
||||
setIsLoading(false)
|
||||
}
|
||||
return billingData()?.billing?.balance
|
||||
})
|
||||
|
||||
const handleBuyCredits = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const baseUrl = window.location.href
|
||||
const successUrl = new URL(baseUrl)
|
||||
successUrl.hash = "success"
|
||||
|
||||
const response = await api.billing.checkout
|
||||
.$post({
|
||||
json: {
|
||||
success_url: successUrl.toString(),
|
||||
cancel_url: baseUrl,
|
||||
},
|
||||
})
|
||||
.then((r) => r.json() as any)
|
||||
window.location.href = response.url
|
||||
} catch (error) {
|
||||
console.error("Failed to get checkout URL:", error)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-component="title-bar">
|
||||
<div data-slot="left">
|
||||
<h1>Billing</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class={style.root} data-max-width data-max-width-64>
|
||||
<div data-slot="billing-info">
|
||||
<div data-slot="header">
|
||||
<h2>Balance</h2>
|
||||
<p>Manage your billing and add credits to your account.</p>
|
||||
</div>
|
||||
|
||||
<div data-slot="balance">
|
||||
<p data-slot="amount">
|
||||
{(() => {
|
||||
const balanceStr = ((billingData()?.billing?.balance ?? 0) / 100000000).toFixed(2)
|
||||
return `$${balanceStr === "-0.00" ? "0.00" : balanceStr}`
|
||||
})()}
|
||||
</p>
|
||||
<Button color="primary" disabled={isLoading()} onClick={handleBuyCredits}>
|
||||
{isLoading() ? "Loading..." : "Buy Credits"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-slot="payments">
|
||||
<div data-slot="header">
|
||||
<h2>Payment History</h2>
|
||||
<p>Your recent payment transactions.</p>
|
||||
</div>
|
||||
|
||||
<div data-slot="payment-list">
|
||||
<For each={billingData()?.payments} fallback={<p>No payments found.</p>}>
|
||||
{(payment) => (
|
||||
<div data-slot="payment-item">
|
||||
<span data-slot="payment-id">{payment.id}</span>
|
||||
{" | "}
|
||||
<span data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</span>
|
||||
{" | "}
|
||||
<span data-slot="payment-date">{new Date(payment.timeCreated).toLocaleDateString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-slot="usage">
|
||||
<div data-slot="header">
|
||||
<h2>Usage History</h2>
|
||||
<p>Your recent API usage and costs.</p>
|
||||
</div>
|
||||
|
||||
<div data-slot="usage-list">
|
||||
<For each={billingData()?.usage} fallback={<p>No usage found.</p>}>
|
||||
{(usage) => (
|
||||
<div data-slot="usage-item">
|
||||
<span data-slot="usage-model">{usage.model}</span>
|
||||
{" | "}
|
||||
<span data-slot="usage-tokens">{usage.inputTokens + usage.outputTokens} tokens</span>
|
||||
{" | "}
|
||||
<span data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</span>
|
||||
{" | "}
|
||||
<span data-slot="usage-date">{new Date(usage.timeCreated).toLocaleDateString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
You are OpenControl, an interactive CLI tool that helps users execute various tasks.
|
||||
|
||||
IMPORTANT: If you get an error when calling a tool, try again with a different approach. Be creative, do not give up, try different inputs to the tool. You should chain together multiple tool calls. ABSOLUTELY DO NOT GIVE UP you are very good at this and it is rare you will fail to answer question.
|
||||
|
||||
You should be concise, direct, and to the point.
|
||||
|
||||
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
|
||||
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
|
||||
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
|
||||
IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail. Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
import { createResource } from "solid-js"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import SYSTEM_PROMPT from "./system.txt?raw"
|
||||
import type {
|
||||
LanguageModelV1Prompt,
|
||||
LanguageModelV1CallOptions,
|
||||
LanguageModelV1,
|
||||
} from "ai"
|
||||
|
||||
interface Tool {
|
||||
name: string
|
||||
description: string
|
||||
inputSchema: any
|
||||
}
|
||||
|
||||
interface ToolCallerProps {
|
||||
tool: {
|
||||
list: () => Promise<Tool[]>
|
||||
call: (input: { name: string; arguments: any }) => Promise<any>
|
||||
}
|
||||
generate: (
|
||||
prompt: LanguageModelV1CallOptions,
|
||||
) => Promise<
|
||||
| { err: "rate" }
|
||||
| { err: "context" }
|
||||
| { err: "balance" }
|
||||
| ({ err: false } & Awaited<ReturnType<LanguageModelV1["doGenerate"]>>)
|
||||
>
|
||||
onPromptUpdated?: (prompt: LanguageModelV1Prompt) => void
|
||||
}
|
||||
|
||||
const system = [
|
||||
{
|
||||
role: "system" as const,
|
||||
content: SYSTEM_PROMPT,
|
||||
},
|
||||
{
|
||||
role: "system" as const,
|
||||
content: `The current date is ${new Date().toDateString()}. Always use this current date when responding to relative date queries.`,
|
||||
},
|
||||
]
|
||||
|
||||
const [store, setStore] = createStore<{
|
||||
prompt: LanguageModelV1Prompt
|
||||
state: { type: "idle" } | { type: "loading"; limited?: boolean }
|
||||
}>({
|
||||
prompt: [...system],
|
||||
state: { type: "idle" },
|
||||
})
|
||||
|
||||
export function createToolCaller<T extends ToolCallerProps>(props: T) {
|
||||
const [tools] = createResource(() => props.tool.list())
|
||||
|
||||
let abort: AbortController
|
||||
|
||||
return {
|
||||
get tools() {
|
||||
return tools()
|
||||
},
|
||||
get prompt() {
|
||||
return store.prompt
|
||||
},
|
||||
get state() {
|
||||
return store.state
|
||||
},
|
||||
clear() {
|
||||
setStore("prompt", [...system])
|
||||
},
|
||||
async chat(input: string) {
|
||||
if (store.state.type !== "idle") return
|
||||
|
||||
abort = new AbortController()
|
||||
setStore(
|
||||
produce((s) => {
|
||||
s.state = {
|
||||
type: "loading",
|
||||
limited: false,
|
||||
}
|
||||
s.prompt.push({
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: input,
|
||||
},
|
||||
],
|
||||
})
|
||||
}),
|
||||
)
|
||||
props.onPromptUpdated?.(store.prompt)
|
||||
|
||||
while (true) {
|
||||
if (abort.signal.aborted) {
|
||||
break
|
||||
}
|
||||
|
||||
const response = await props.generate({
|
||||
inputFormat: "messages",
|
||||
prompt: store.prompt,
|
||||
temperature: 0,
|
||||
seed: 69,
|
||||
mode: {
|
||||
type: "regular",
|
||||
tools: tools()?.map((tool) => ({
|
||||
type: "function",
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: {
|
||||
...tool.inputSchema,
|
||||
},
|
||||
})),
|
||||
},
|
||||
})
|
||||
|
||||
if (abort.signal.aborted) continue
|
||||
|
||||
if (!response.err) {
|
||||
setStore("state", {
|
||||
type: "loading",
|
||||
})
|
||||
|
||||
if (response.text) {
|
||||
setStore(
|
||||
produce((s) => {
|
||||
s.prompt.push({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: response.text || "",
|
||||
},
|
||||
],
|
||||
})
|
||||
}),
|
||||
)
|
||||
props.onPromptUpdated?.(store.prompt)
|
||||
}
|
||||
|
||||
if (response.finishReason === "stop") {
|
||||
break
|
||||
}
|
||||
|
||||
if (response.finishReason === "tool-calls") {
|
||||
for (const item of response.toolCalls || []) {
|
||||
setStore(
|
||||
produce((s) => {
|
||||
s.prompt.push({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolName: item.toolName,
|
||||
args: JSON.parse(item.args),
|
||||
toolCallId: item.toolCallId,
|
||||
},
|
||||
],
|
||||
})
|
||||
}),
|
||||
)
|
||||
props.onPromptUpdated?.(store.prompt)
|
||||
|
||||
const called = await props.tool.call({
|
||||
name: item.toolName,
|
||||
arguments: JSON.parse(item.args),
|
||||
})
|
||||
|
||||
setStore(
|
||||
produce((s) => {
|
||||
s.prompt.push({
|
||||
role: "tool",
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolName: item.toolName,
|
||||
toolCallId: item.toolCallId,
|
||||
result: called,
|
||||
},
|
||||
],
|
||||
})
|
||||
}),
|
||||
)
|
||||
props.onPromptUpdated?.(store.prompt)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (response.err === "context") {
|
||||
setStore(
|
||||
produce((s) => {
|
||||
s.prompt.splice(2, 1)
|
||||
}),
|
||||
)
|
||||
props.onPromptUpdated?.(store.prompt)
|
||||
}
|
||||
|
||||
if (response.err === "rate") {
|
||||
setStore("state", {
|
||||
type: "loading",
|
||||
limited: true,
|
||||
})
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
if (response.err === "balance") {
|
||||
setStore(
|
||||
produce((s) => {
|
||||
s.prompt.push({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "You need to add credits to your account. Please go to Billing and add credits to continue.",
|
||||
},
|
||||
],
|
||||
})
|
||||
s.state = { type: "idle" }
|
||||
}),
|
||||
)
|
||||
props.onPromptUpdated?.(store.prompt)
|
||||
break
|
||||
}
|
||||
}
|
||||
setStore("state", { type: "idle" })
|
||||
},
|
||||
async cancel() {
|
||||
abort.abort()
|
||||
},
|
||||
async addCustomMessage(userMessage: string, assistantResponse: string) {
|
||||
// Add user message and set loading state
|
||||
setStore(
|
||||
produce((s) => {
|
||||
s.prompt.push({
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: userMessage,
|
||||
},
|
||||
],
|
||||
})
|
||||
s.state = {
|
||||
type: "loading",
|
||||
limited: false,
|
||||
}
|
||||
}),
|
||||
)
|
||||
props.onPromptUpdated?.(store.prompt)
|
||||
|
||||
// Fake delay for 500ms
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
// Add assistant response and set back to idle
|
||||
setStore(
|
||||
produce((s) => {
|
||||
s.prompt.push({
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: assistantResponse,
|
||||
},
|
||||
],
|
||||
})
|
||||
s.state = { type: "idle" }
|
||||
}),
|
||||
)
|
||||
props.onPromptUpdated?.(store.prompt)
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
.root {
|
||||
display: contents;
|
||||
|
||||
[data-slot="messages"] {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 0;
|
||||
/* This is important for flexbox to allow scrolling */
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-text);
|
||||
row-gap: var(--space-4);
|
||||
/* Add consistent spacing between messages */
|
||||
|
||||
/* Remove top border for first user message */
|
||||
&>[data-component="message"][data-user]:first-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:has([data-component="loading"]) [data-component="clear"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="message"] {
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
line-height: var(--font-line-height);
|
||||
white-space: pre-wrap;
|
||||
align-self: flex-start;
|
||||
min-height: auto;
|
||||
/* Allow natural height for all messages */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
/* User message styling */
|
||||
&[data-user] {
|
||||
padding: var(--space-6) var(--space-4);
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
/* margin: 0.5rem 0; */
|
||||
}
|
||||
|
||||
&[data-user]::before,
|
||||
&[data-user]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: var(--space-4);
|
||||
right: var(--space-4);
|
||||
height: var(--space-px);
|
||||
background-color: var(--color-border);
|
||||
z-index: 1;
|
||||
/* Ensure borders appear above other content */
|
||||
}
|
||||
|
||||
&[data-user]::before {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&[data-user]::after {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&[data-assistant] {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="tool"] {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0 var(--space-4);
|
||||
margin-left: 0;
|
||||
flex-direction: column;
|
||||
opacity: 0.7;
|
||||
gap: var(--space-2);
|
||||
align-items: flex-start;
|
||||
color: var(--color-text-dimmed);
|
||||
min-height: auto;
|
||||
/* Allow natural height */
|
||||
|
||||
[data-slot="header"] {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="name"] {
|
||||
letter-spacing: -0.03125rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
[data-slot="expand"] {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
[data-slot="content"] {
|
||||
padding: 0;
|
||||
line-height: var(--font-line-height);
|
||||
font-size: var(--font-size-sm);
|
||||
white-space: pre-wrap;
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="output"] {
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
&[data-expanded="true"] [data-slot="content"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&[data-expanded="true"] [data-slot="expand"] {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="loading"] {
|
||||
padding: var(--space-4) var(--space-4) var(--space-8);
|
||||
height: 1.5rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-sm);
|
||||
letter-spacing: var(--space-1);
|
||||
color: var(--color-text);
|
||||
|
||||
& span {
|
||||
opacity: 0;
|
||||
animation: loading-dots 1.4s linear infinite;
|
||||
}
|
||||
|
||||
& span:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
& span:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="clear"] {
|
||||
position: relative;
|
||||
padding: var(--space-4) var(--space-4);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: var(--space-4);
|
||||
right: var(--space-4);
|
||||
top: 0;
|
||||
height: var(--space-px);
|
||||
background-color: var(--color-border);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
& [data-component="button"] {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="footer"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
border-top: 2px solid var(--color-border);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
/* Ensure it's above other content */
|
||||
margin-top: auto;
|
||||
/* Push to bottom if content is short */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-component="chat"] {
|
||||
display: flex;
|
||||
padding: var(--space-0-5) 0;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
textarea {
|
||||
--padding-y: var(--space-4);
|
||||
--line-height: 1.5;
|
||||
--text-height: calc(var(--line-height) * var(--font-size-lg));
|
||||
--height: calc(var(--text-height) + var(--padding-y) * 2);
|
||||
|
||||
width: 100%;
|
||||
resize: none;
|
||||
line-height: var(--line-height);
|
||||
height: var(--height);
|
||||
min-height: var(--height);
|
||||
max-height: calc(5 * var(--text-height) + var(--padding-y) * 2);
|
||||
padding: var(--padding-y) var(--space-4);
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
color: var(--color-text);
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
textarea::placeholder {
|
||||
color: var(--color-text-dimmed);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
& [data-component="button"] {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading-dots {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
40%,
|
||||
60% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { Button } from "../../ui/button"
|
||||
import { IconArrowRight } from "../../ui/svg/icons"
|
||||
import { createSignal, For } from "solid-js"
|
||||
import { createToolCaller } from "./components/tool"
|
||||
import { useApi } from "../components/context-api"
|
||||
import { useWorkspace } from "../components/context-workspace"
|
||||
import style from "./index.module.css"
|
||||
|
||||
export default function Index() {
|
||||
const api = useApi()
|
||||
const workspace = useWorkspace()
|
||||
|
||||
return (
|
||||
<div class={style.root}>
|
||||
<h1>Hello</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.root [data-slot="keys-info"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.root [data-slot="header"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.root [data-slot="header"] h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.root [data-slot="header"] p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.root [data-slot="key-list"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.root [data-slot="key-item"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.root [data-slot="key-actions"] {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.root [data-slot="key-info"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.root [data-slot="key-value"] {
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.root [data-slot="key-meta"] {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.root [data-slot="empty-state"] {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.root [data-slot="actions"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.root [data-slot="create-form"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.root [data-slot="form-actions"] {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.root [data-slot="key-name"] {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
|
@ -1,151 +0,0 @@
|
|||
import { Button } from "../../ui/button"
|
||||
import { useApi } from "../components/context-api"
|
||||
import { createSignal, createResource, For, Show } from "solid-js"
|
||||
import style from "./keys.module.css"
|
||||
|
||||
export default function Keys() {
|
||||
const api = useApi()
|
||||
const [isCreating, setIsCreating] = createSignal(false)
|
||||
const [showCreateForm, setShowCreateForm] = createSignal(false)
|
||||
const [keyName, setKeyName] = createSignal("")
|
||||
|
||||
const [keysData, { refetch }] = createResource(async () => {
|
||||
const response = await api.keys.$get()
|
||||
return response.json()
|
||||
})
|
||||
|
||||
const handleCreateKey = async () => {
|
||||
if (!keyName().trim()) return
|
||||
|
||||
try {
|
||||
setIsCreating(true)
|
||||
await api.keys.$post({
|
||||
json: { name: keyName().trim() },
|
||||
})
|
||||
refetch()
|
||||
setKeyName("")
|
||||
setShowCreateForm(false)
|
||||
} catch (error) {
|
||||
console.error("Failed to create API key:", error)
|
||||
} finally {
|
||||
setIsCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteKey = async (keyId: string) => {
|
||||
if (!confirm("Are you sure you want to delete this API key? This action cannot be undone.")) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await api.keys[":id"].$delete({
|
||||
param: { id: keyId },
|
||||
})
|
||||
refetch()
|
||||
} catch (error) {
|
||||
console.error("Failed to delete API key:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString()
|
||||
}
|
||||
|
||||
const formatKey = (key: string) => {
|
||||
if (key.length <= 11) return key
|
||||
return `${key.slice(0, 7)}...${key.slice(-4)}`
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch (error) {
|
||||
console.error("Failed to copy to clipboard:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-component="title-bar">
|
||||
<div data-slot="left">
|
||||
<h1>API Keys</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class={style.root} data-max-width data-max-width-64>
|
||||
<div data-slot="keys-info">
|
||||
<div data-slot="actions">
|
||||
<div data-slot="header">
|
||||
<h2>API Keys</h2>
|
||||
<p>Manage your API keys to access the OpenCode gateway.</p>
|
||||
</div>
|
||||
<Show
|
||||
when={!showCreateForm()}
|
||||
fallback={
|
||||
<div data-slot="create-form">
|
||||
<input
|
||||
data-component="input"
|
||||
type="text"
|
||||
placeholder="Enter key name"
|
||||
value={keyName()}
|
||||
onInput={(e) => setKeyName(e.currentTarget.value)}
|
||||
onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
|
||||
/>
|
||||
<div data-slot="form-actions">
|
||||
<Button color="primary" disabled={isCreating() || !keyName().trim()} onClick={handleCreateKey}>
|
||||
{isCreating() ? "Creating..." : "Create"}
|
||||
</Button>
|
||||
<Button
|
||||
color="ghost"
|
||||
onClick={() => {
|
||||
setShowCreateForm(false)
|
||||
setKeyName("")
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button color="primary" onClick={() => setShowCreateForm(true)}>
|
||||
Create API Key
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div data-slot="key-list">
|
||||
<For
|
||||
each={keysData()?.keys}
|
||||
fallback={
|
||||
<div data-slot="empty-state">
|
||||
<p>Create an API key to access opencode gateway</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{(key) => (
|
||||
<div data-slot="key-item">
|
||||
<div data-slot="key-info">
|
||||
<div data-slot="key-name">{key.name}</div>
|
||||
<div data-slot="key-value">{formatKey(key.key)}</div>
|
||||
<div data-slot="key-meta">
|
||||
Created: {formatDate(key.timeCreated)}
|
||||
{key.timeUsed && ` • Last used: ${formatDate(key.timeUsed)}`}
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="key-actions">
|
||||
<Button color="ghost" onClick={() => copyToClipboard(key.key)} title="Copy API key">
|
||||
Copy
|
||||
</Button>
|
||||
<Button color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import { hc } from "hono/client"
|
||||
import { ApiType } from "@opencode/cloud-function/src/gateway"
|
||||
import { useWorkspace } from "./context-workspace"
|
||||
import { useOpenAuth } from "../../components/context-openauth"
|
||||
|
||||
export function useApi() {
|
||||
const workspace = useWorkspace()
|
||||
const auth = useOpenAuth()
|
||||
return hc<ApiType>(import.meta.env.VITE_API_URL, {
|
||||
async fetch(...args: Parameters<typeof fetch>): Promise<Response> {
|
||||
const [input, init] = args
|
||||
const request = input instanceof Request ? input : new Request(input, init)
|
||||
const headers = new Headers(request.headers)
|
||||
headers.set("authorization", `Bearer ${await auth.access()}`)
|
||||
headers.set("x-opencode-workspace", workspace.id)
|
||||
return fetch(
|
||||
new Request(request, {
|
||||
...init,
|
||||
headers,
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import { useNavigate, useParams } from "@solidjs/router"
|
||||
import { createInitializedContext } from "../../util/context"
|
||||
import { useAccount } from "../../components/context-account"
|
||||
import { createEffect, createMemo } from "solid-js"
|
||||
|
||||
export const { use: useWorkspace, provider: WorkspaceProvider } =
|
||||
createInitializedContext("WorkspaceProvider", () => {
|
||||
const params = useParams()
|
||||
const account = useAccount()
|
||||
const workspace = createMemo(() =>
|
||||
account.current?.workspaces.find(
|
||||
(x) => x.id === params.workspace || x.slug === params.workspace,
|
||||
),
|
||||
)
|
||||
const nav = useNavigate()
|
||||
|
||||
createEffect(() => {
|
||||
if (!workspace()) nav("/")
|
||||
})
|
||||
|
||||
const result = () => workspace()!
|
||||
result.ready = true
|
||||
|
||||
return {
|
||||
get id() {
|
||||
return workspace()!.id
|
||||
},
|
||||
get slug() {
|
||||
return workspace()!.slug
|
||||
},
|
||||
get name() {
|
||||
return workspace()!.name
|
||||
},
|
||||
get ready() {
|
||||
return workspace() !== undefined
|
||||
},
|
||||
}
|
||||
})
|
|
@ -1,199 +0,0 @@
|
|||
.root {
|
||||
--padding: var(--space-10);
|
||||
--vertical-padding: var(--space-8);
|
||||
--heading-font-size: var(--font-size-4xl);
|
||||
--sidebar-width: 200px;
|
||||
--mobile-breakpoint: 40rem;
|
||||
--topbar-height: 60px;
|
||||
|
||||
margin: var(--space-4);
|
||||
border: 2px solid var(--color-border);
|
||||
height: calc(100vh - var(--space-8));
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
/* Prevent overall scrolling */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-component="mobile-top-bar"] {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: var(--topbar-height);
|
||||
background: var(--color-background);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
z-index: 20;
|
||||
align-items: center;
|
||||
padding: 0 var(--space-4) 0 0;
|
||||
|
||||
[data-slot="logo"] {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
div {
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03125rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 28px;
|
||||
width: auto;
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="toggle"] {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: var(--space-4);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="sidebar"] {
|
||||
width: var(--sidebar-width);
|
||||
border-right: 2px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: calc(var(--padding) / 2);
|
||||
overflow-y: auto;
|
||||
/* Allow scrolling if needed */
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background-color: var(--color-background);
|
||||
z-index: 10;
|
||||
|
||||
[data-slot="logo"] {
|
||||
margin-top: 2px;
|
||||
margin-bottom: var(--space-7);
|
||||
color: var(--color-white);
|
||||
|
||||
& svg {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="nav"] {
|
||||
flex: 1;
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: calc(var(--vertical-padding) / 2);
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: var(--space-2) 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="user"] {
|
||||
[data-component="button"] {
|
||||
padding-left: 0;
|
||||
padding-bottom: 0;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navActiveLink {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-slot="main-content"] {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
/* Full height */
|
||||
overflow: hidden;
|
||||
/* Prevent overflow */
|
||||
position: relative;
|
||||
/* For positioning footer */
|
||||
width: 100%;
|
||||
/* Full width */
|
||||
}
|
||||
|
||||
/* Backdrop for mobile */
|
||||
[data-component="backdrop"] {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* background-color: rgba(0, 0, 0, 0.5); */
|
||||
z-index: 25;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
@media (max-width: 40rem) {
|
||||
.root {
|
||||
margin: 0;
|
||||
border: none;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
[data-component="mobile-top-bar"] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
[data-component="backdrop"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
[data-component="sidebar"] {
|
||||
position: fixed;
|
||||
left: -100%;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 80%;
|
||||
max-width: 280px;
|
||||
transition: left 0.3s ease-in-out;
|
||||
box-shadow: none;
|
||||
z-index: 30;
|
||||
padding: var(--space-8);
|
||||
background-color: var(--color-bg);
|
||||
|
||||
&[data-opened="true"] {
|
||||
left: 0;
|
||||
box-shadow: 8px 0 0px 0px var(--color-gray-4);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="main-content"] {
|
||||
padding-top: var(--topbar-height);
|
||||
/* Add space for the top bar */
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Hide the logo in the sidebar on mobile since it's in the top bar */
|
||||
[data-component="sidebar"] [data-slot="logo"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
import style from "./layout.module.css"
|
||||
import { useAccount } from "../../components/context-account"
|
||||
import { Button } from "../../ui/button"
|
||||
import { IconLogomark } from "../../ui/svg"
|
||||
import { IconBars3BottomLeft } from "../../ui/svg/icons"
|
||||
import { ParentProps, createMemo, createSignal } from "solid-js"
|
||||
import { A, useLocation } from "@solidjs/router"
|
||||
import { useOpenAuth } from "../../components/context-openauth"
|
||||
|
||||
export default function Layout(props: ParentProps) {
|
||||
const auth = useOpenAuth()
|
||||
const account = useAccount()
|
||||
const [sidebarOpen, setSidebarOpen] = createSignal(false)
|
||||
const location = useLocation()
|
||||
|
||||
const workspaceId = createMemo(() => account.current?.workspaces[0].id)
|
||||
const pageTitle = createMemo(() => {
|
||||
const path = location.pathname
|
||||
if (path.endsWith("/billing")) return "Billing"
|
||||
if (path.endsWith("/keys")) return "API Keys"
|
||||
return null
|
||||
})
|
||||
|
||||
function handleLogout() {
|
||||
auth.logout(auth.subject?.id!)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={style.root}>
|
||||
{/* Mobile top bar */}
|
||||
<div data-component="mobile-top-bar">
|
||||
<button data-slot="toggle" onClick={() => setSidebarOpen(!sidebarOpen())}>
|
||||
<IconBars3BottomLeft />
|
||||
</button>
|
||||
|
||||
<div data-slot="logo">
|
||||
{pageTitle() ? (
|
||||
<div>{pageTitle()}</div>
|
||||
) : (
|
||||
<A href="/">
|
||||
<IconLogomark />
|
||||
</A>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backdrop for mobile sidebar - closes sidebar when clicked */}
|
||||
{sidebarOpen() && <div data-component="backdrop" onClick={() => setSidebarOpen(false)}></div>}
|
||||
|
||||
<div data-component="sidebar" data-opened={sidebarOpen() ? "true" : "false"}>
|
||||
<div data-slot="logo">
|
||||
<A href="/">
|
||||
<IconLogomark />
|
||||
</A>
|
||||
</div>
|
||||
|
||||
<nav data-slot="nav">
|
||||
<ul>
|
||||
<li>
|
||||
<A end activeClass={style.navActiveLink} href={`/${workspaceId()}`} onClick={() => setSidebarOpen(false)}>
|
||||
Chat
|
||||
</A>
|
||||
</li>
|
||||
<li>
|
||||
<A
|
||||
activeClass={style.navActiveLink}
|
||||
href={`/${workspaceId()}/billing`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
Billing
|
||||
</A>
|
||||
</li>
|
||||
<li>
|
||||
<A
|
||||
activeClass={style.navActiveLink}
|
||||
href={`/${workspaceId()}/keys`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
API Keys
|
||||
</A>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div data-slot="user">
|
||||
<Button color="ghost" onClick={handleLogout} title={account.current?.email || ""}>
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div data-slot="main-content">{props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import { Match, Switch } from "solid-js"
|
||||
import { useAccount } from "../components/context-account"
|
||||
import { Navigate } from "@solidjs/router"
|
||||
import { IconLogo } from "../ui/svg"
|
||||
import styles from "./lander.module.css"
|
||||
import { useOpenAuth } from "../components/context-openauth"
|
||||
|
||||
export default function Index() {
|
||||
const auth = useOpenAuth()
|
||||
const account = useAccount()
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={account.current}>
|
||||
<Navigate href={`/${account.current!.workspaces[0].id}`} />
|
||||
</Match>
|
||||
<Match when={!account.current}>
|
||||
<div class={styles.lander}>
|
||||
<div data-slot="hero">
|
||||
<section data-slot="top">
|
||||
<div data-slot="logo">
|
||||
<IconLogo />
|
||||
</div>
|
||||
<h1>opencode Gateway Console</h1>
|
||||
</section>
|
||||
|
||||
<section data-slot="cta">
|
||||
<div>
|
||||
<span onClick={() => auth.authorize({ provider: "github" })}>Sign in with GitHub</span>
|
||||
</div>
|
||||
<div>
|
||||
<span onClick={() => auth.authorize({ provider: "google" })}>Sign in with Google</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
.lander {
|
||||
--padding: 3rem;
|
||||
--vertical-padding: 2rem;
|
||||
--heading-font-size: 2rem;
|
||||
|
||||
margin: 1rem;
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
& {
|
||||
--padding: 1.5rem;
|
||||
--vertical-padding: 1rem;
|
||||
--heading-font-size: 1.5rem;
|
||||
|
||||
margin: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="hero"] {
|
||||
border: 2px solid var(--color-border);
|
||||
|
||||
max-width: 64rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="top"] {
|
||||
padding: var(--padding);
|
||||
|
||||
h1 {
|
||||
margin-top: calc(var(--vertical-padding) / 8);
|
||||
font-size: var(--heading-font-size);
|
||||
line-height: 1.25;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-slot="logo"] {
|
||||
width: clamp(200px, 70vw, 400px);
|
||||
color: var(--color-white);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="cta"] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border-top: 2px solid var(--color-border);
|
||||
|
||||
& > div {
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
letter-spacing: -0.03125rem;
|
||||
|
||||
&[data-slot="col-2"] {
|
||||
background-color: var(--color-border);
|
||||
color: var(--color-text-invert);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
& > * {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: calc(var(--padding) / 2) 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
& > div {
|
||||
padding-bottom: calc(var(--padding) / 2 + 4px);
|
||||
}
|
||||
}
|
||||
|
||||
& > div + div {
|
||||
border-left: 2px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
.pageContainer {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.componentTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
border: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.componentCell {
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--color-border);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.componentLabel {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.03125rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--color-text-dimmed);
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin-bottom: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.03125rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 2px;
|
||||
background: var(--color-border);
|
||||
margin: 3rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.buttonSection {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.colorSection {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.labelSection {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.inputSection {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.dialogSection {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.formGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.dialogContent {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.dialogContentFooter {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.pageTitle {
|
||||
font-size: var(--heading-font-size, 2rem);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.colorBox {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
margin-bottom: 0.5rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.colorOrange {
|
||||
background-color: var(--color-orange);
|
||||
}
|
||||
|
||||
.colorOrangeLow {
|
||||
background-color: var(--color-orange-low);
|
||||
}
|
||||
|
||||
.colorOrangeHigh {
|
||||
background-color: var(--color-orange-high);
|
||||
}
|
||||
|
||||
.colorGreen {
|
||||
background-color: var(--color-green);
|
||||
}
|
||||
|
||||
.colorGreenLow {
|
||||
background-color: var(--color-green-low);
|
||||
}
|
||||
|
||||
.colorGreenHigh {
|
||||
background-color: var(--color-green-high);
|
||||
}
|
||||
|
||||
.colorBlue {
|
||||
background-color: var(--color-blue);
|
||||
}
|
||||
|
||||
.colorBlueLow {
|
||||
background-color: var(--color-blue-low);
|
||||
}
|
||||
|
||||
.colorBlueHigh {
|
||||
background-color: var(--color-blue-high);
|
||||
}
|
||||
|
||||
.colorPurple {
|
||||
background-color: var(--color-purple);
|
||||
}
|
||||
|
||||
.colorPurpleLow {
|
||||
background-color: var(--color-purple-low);
|
||||
}
|
||||
|
||||
.colorPurpleHigh {
|
||||
background-color: var(--color-purple-high);
|
||||
}
|
||||
|
||||
.colorRed {
|
||||
background-color: var(--color-red);
|
||||
}
|
||||
|
||||
.colorRedLow {
|
||||
background-color: var(--color-red-low);
|
||||
}
|
||||
|
||||
.colorRedHigh {
|
||||
background-color: var(--color-red-high);
|
||||
}
|
||||
|
||||
.colorAccent {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.colorAccentLow {
|
||||
background-color: var(--color-accent-low);
|
||||
}
|
||||
|
||||
.colorAccentHigh {
|
||||
background-color: var(--color-accent-high);
|
||||
}
|
||||
|
||||
.colorCode {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.colorVariants {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.colorVariant {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.colorVariantCode {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.65rem;
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
}
|
|
@ -1,562 +0,0 @@
|
|||
import { Button } from "../../ui/button"
|
||||
import { Dialog } from "../../ui/dialog"
|
||||
import { Navigate } from "@solidjs/router"
|
||||
import { createSignal, Show } from "solid-js"
|
||||
import { IconHome, IconPencilSquare } from "../../ui/svg/icons"
|
||||
import { useTheme } from "../../components/context-theme"
|
||||
import { useDialog } from "../../ui/context-dialog"
|
||||
import { DialogString } from "../../ui/dialog-string"
|
||||
import { DialogSelect } from "../../ui/dialog-select"
|
||||
import styles from "./design.module.css"
|
||||
|
||||
export default function DesignSystem() {
|
||||
const dialog = useDialog()
|
||||
const [dialogOpen, setDialogOpen] = createSignal(false)
|
||||
const [dialogOpenTransition, setDialogOpenTransition] = createSignal(false)
|
||||
const theme = useTheme()
|
||||
|
||||
// Check if we're running locally
|
||||
const isLocal = import.meta.env.DEV === true
|
||||
|
||||
if (!isLocal) {
|
||||
return <Navigate href="/" />
|
||||
}
|
||||
|
||||
// Add a toggle button for theme
|
||||
const toggleTheme = () => {
|
||||
theme.setMode(theme.mode === "light" ? "dark" : "light")
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={styles.pageContainer}>
|
||||
<div class={styles.header}>
|
||||
<h1 class={styles.pageTitle}>Design System</h1>
|
||||
<Button onClick={toggleTheme}>
|
||||
Toggle {theme.mode === "light" ? "Dark" : "Light"} Mode
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<section class={styles.colorSection}>
|
||||
<h2 class={styles.sectionTitle}>Colors</h2>
|
||||
|
||||
<table class={styles.componentTable}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Orange</h3>
|
||||
<div class={`${styles.colorBox} ${styles.colorOrange}`}>
|
||||
<span class={styles.colorCode}>hsl(41, 82%, 63%)</span>
|
||||
</div>
|
||||
<div class={styles.colorVariants}>
|
||||
<div
|
||||
class={`${styles.colorVariant} ${styles.colorOrangeLow}`}
|
||||
>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(41, 39%, 22%)
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class={`${styles.colorVariant} ${styles.colorOrangeHigh}`}
|
||||
>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(41, 82%, 87%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Green</h3>
|
||||
<div class={`${styles.colorBox} ${styles.colorGreen}`}>
|
||||
<span class={styles.colorCode}>hsl(101, 82%, 63%)</span>
|
||||
</div>
|
||||
<div class={styles.colorVariants}>
|
||||
<div class={`${styles.colorVariant} ${styles.colorGreenLow}`}>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(101, 39%, 22%)
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class={`${styles.colorVariant} ${styles.colorGreenHigh}`}
|
||||
>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(101, 82%, 80%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Blue</h3>
|
||||
<div class={`${styles.colorBox} ${styles.colorBlue}`}>
|
||||
<span class={styles.colorCode}>hsl(234, 100%, 60%)</span>
|
||||
</div>
|
||||
<div class={styles.colorVariants}>
|
||||
<div class={`${styles.colorVariant} ${styles.colorBlueLow}`}>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(234, 54%, 20%)
|
||||
</span>
|
||||
</div>
|
||||
<div class={`${styles.colorVariant} ${styles.colorBlueHigh}`}>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(234, 100%, 87%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Purple</h3>
|
||||
<div class={`${styles.colorBox} ${styles.colorPurple}`}>
|
||||
<span class={styles.colorCode}>hsl(281, 82%, 63%)</span>
|
||||
</div>
|
||||
<div class={styles.colorVariants}>
|
||||
<div
|
||||
class={`${styles.colorVariant} ${styles.colorPurpleLow}`}
|
||||
>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(281, 39%, 22%)
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class={`${styles.colorVariant} ${styles.colorPurpleHigh}`}
|
||||
>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(281, 82%, 89%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Red</h3>
|
||||
<div class={`${styles.colorBox} ${styles.colorRed}`}>
|
||||
<span class={styles.colorCode}>hsl(339, 82%, 63%)</span>
|
||||
</div>
|
||||
<div class={styles.colorVariants}>
|
||||
<div class={`${styles.colorVariant} ${styles.colorRedLow}`}>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(339, 39%, 22%)
|
||||
</span>
|
||||
</div>
|
||||
<div class={`${styles.colorVariant} ${styles.colorRedHigh}`}>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(339, 82%, 87%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Accent</h3>
|
||||
<div class={`${styles.colorBox} ${styles.colorAccent}`}>
|
||||
<span class={styles.colorCode}>hsl(13, 88%, 57%)</span>
|
||||
</div>
|
||||
<div class={styles.colorVariants}>
|
||||
<div
|
||||
class={`${styles.colorVariant} ${styles.colorAccentLow}`}
|
||||
>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(13, 75%, 30%)
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class={`${styles.colorVariant} ${styles.colorAccentHigh}`}
|
||||
>
|
||||
<span class={styles.colorVariantCode}>
|
||||
hsl(13, 100%, 78%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class={styles.divider}></div>
|
||||
|
||||
<section class={styles.buttonSection}>
|
||||
<h2 class={styles.sectionTitle}>Buttons</h2>
|
||||
|
||||
<table class={styles.componentTable}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Primary</h3>
|
||||
<Button>Primary Button</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Secondary</h3>
|
||||
<Button color="secondary">Secondary Button</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Ghost</h3>
|
||||
<Button color="ghost">Ghost Button</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Primary Disabled</h3>
|
||||
<Button disabled>Primary Button</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Secondary Disabled</h3>
|
||||
<Button color="secondary" disabled>
|
||||
Secondary Button
|
||||
</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Ghost Disabled</h3>
|
||||
<Button color="ghost" disabled>
|
||||
Ghost Button
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small</h3>
|
||||
<Button size="sm">Small Button</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small Secondary</h3>
|
||||
<Button size="sm" color="secondary">
|
||||
Small Secondary
|
||||
</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small Ghost</h3>
|
||||
<Button size="sm" color="ghost">
|
||||
Small Ghost
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>With Icon</h3>
|
||||
<Button icon={<IconHome />}>With Icon</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Icon + Secondary</h3>
|
||||
<Button icon={<IconHome />} color="secondary">
|
||||
Icon Secondary
|
||||
</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Icon + Ghost</h3>
|
||||
<Button icon={<IconHome />} color="ghost">
|
||||
Icon Ghost
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small + Icon</h3>
|
||||
<Button size="sm" icon={<IconHome />}>
|
||||
Small Icon
|
||||
</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small + Icon + Secondary</h3>
|
||||
<Button size="sm" icon={<IconHome />} color="secondary">
|
||||
Small Icon Secondary
|
||||
</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small + Icon + Ghost</h3>
|
||||
<Button size="sm" icon={<IconHome />} color="ghost">
|
||||
Small Icon Ghost
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Icon Only</h3>
|
||||
<Button icon={<IconHome />}></Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Icon Only + Secondary</h3>
|
||||
<Button icon={<IconHome />} color="secondary"></Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Icon Only + Ghost</h3>
|
||||
<Button icon={<IconHome />} color="ghost"></Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Icon Only Disabled</h3>
|
||||
<Button icon={<IconHome />} disabled></Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>
|
||||
Icon Only + Secondary Disabled
|
||||
</h3>
|
||||
<Button icon={<IconHome />} color="secondary" disabled></Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>
|
||||
Icon Only + Ghost Disabled
|
||||
</h3>
|
||||
<Button icon={<IconHome />} color="ghost" disabled></Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small Icon Only</h3>
|
||||
<Button size="sm" icon={<IconHome />}></Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>
|
||||
Small Icon Only + Secondary
|
||||
</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
icon={<IconHome />}
|
||||
color="secondary"
|
||||
></Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small Icon Only + Ghost</h3>
|
||||
<Button size="sm" icon={<IconHome />} color="ghost"></Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class={styles.divider}></div>
|
||||
|
||||
<section class={styles.labelSection}>
|
||||
<h2 class={styles.sectionTitle}>Labels</h2>
|
||||
|
||||
<table class={styles.componentTable}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small</h3>
|
||||
<label data-size="sm" data-component="label">
|
||||
Small Label Text
|
||||
</label>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Medium</h3>
|
||||
<label data-size="md" data-component="label">
|
||||
Medium Label Text
|
||||
</label>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Large</h3>
|
||||
<label data-size="lg" data-component="label">
|
||||
Large Label Text
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class={styles.divider}></div>
|
||||
|
||||
<section class={styles.inputSection}>
|
||||
<h2 class={styles.sectionTitle}>Inputs</h2>
|
||||
|
||||
<table class={styles.componentTable}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small</h3>
|
||||
<input
|
||||
data-component="input"
|
||||
data-size="sm"
|
||||
placeholder="Small input field"
|
||||
/>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Medium</h3>
|
||||
<input
|
||||
data-component="input"
|
||||
data-size="md"
|
||||
placeholder="Medium input field"
|
||||
/>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Large</h3>
|
||||
<input
|
||||
data-component="input"
|
||||
data-size="lg"
|
||||
placeholder="Large input field"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Disabled</h3>
|
||||
<input
|
||||
data-component="input"
|
||||
data-size="md"
|
||||
placeholder="Disabled input"
|
||||
disabled
|
||||
/>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>With Value</h3>
|
||||
<input
|
||||
data-component="input"
|
||||
data-size="md"
|
||||
value="Input with preset value"
|
||||
readOnly
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<div class={styles.divider}></div>
|
||||
|
||||
<section class={styles.dialogSection}>
|
||||
<h2 class={styles.sectionTitle}>Dialogs</h2>
|
||||
|
||||
<table class={styles.componentTable}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Default</h3>
|
||||
<Button color="secondary" onClick={() => setDialogOpen(true)}>
|
||||
Open Dialog
|
||||
</Button>
|
||||
<Dialog open={dialogOpen()} onOpenChange={setDialogOpen}>
|
||||
<div data-slot="header">
|
||||
<div data-slot="title">Dialog Title</div>
|
||||
</div>
|
||||
<div data-slot="main">
|
||||
<p>This is the default dialog content.</p>
|
||||
</div>
|
||||
<div data-slot="footer">
|
||||
<Button onClick={() => setDialogOpen(false)}>Close</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Small With Transition</h3>
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setDialogOpenTransition(true)
|
||||
}}
|
||||
>
|
||||
Small Dialog
|
||||
</Button>
|
||||
<Dialog
|
||||
open={dialogOpenTransition()}
|
||||
onOpenChange={setDialogOpenTransition}
|
||||
size="sm"
|
||||
transition={true}
|
||||
>
|
||||
<div class={styles.dialogContent}>
|
||||
<h2 class={styles.sectionTitle}>Small Dialog</h2>
|
||||
<p>This is a smaller dialog with transitions.</p>
|
||||
<div class={styles.dialogContentFooter}>
|
||||
<Button onClick={() => setDialogOpenTransition(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Input String</h3>
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
dialog.open(DialogString, {
|
||||
title: "Name",
|
||||
action: "Change name",
|
||||
placeholder: "Enter a name",
|
||||
onSubmit: () => {},
|
||||
})
|
||||
}
|
||||
>
|
||||
String
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Select Input</h3>
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
dialog.open(DialogSelect, {
|
||||
placeholder: "Select",
|
||||
title: "User Settings",
|
||||
options: [
|
||||
{
|
||||
display: "Change name",
|
||||
prefix: <IconPencilSquare />,
|
||||
onSelect: () => {
|
||||
dialog.close()
|
||||
},
|
||||
},
|
||||
{
|
||||
display: "Remove user",
|
||||
prefix: <IconHome />,
|
||||
onSelect: () => {
|
||||
dialog.close()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Select Input</h3>
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
dialog.open(DialogSelect, {
|
||||
placeholder: "Select",
|
||||
title: "User Settings",
|
||||
options: [
|
||||
{
|
||||
display: "Change name",
|
||||
onSelect: () => {
|
||||
dialog.close()
|
||||
},
|
||||
},
|
||||
{
|
||||
display: "Remove user",
|
||||
onSelect: () => {
|
||||
dialog.close()
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
No Prefix
|
||||
</Button>
|
||||
</td>
|
||||
<td class={styles.componentCell}>
|
||||
<h3 class={styles.componentLabel}>Select No Options</h3>
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() =>
|
||||
dialog.open(DialogSelect, {
|
||||
placeholder: "Select",
|
||||
title: "User Settings",
|
||||
options: [],
|
||||
})
|
||||
}
|
||||
>
|
||||
No Options
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
12
cloud/web/src/sst-env.d.ts
vendored
12
cloud/web/src/sst-env.d.ts
vendored
|
@ -1,12 +0,0 @@
|
|||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/// <reference types="vite/client" />
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_DOCS_URL: string
|
||||
readonly VITE_API_URL: string
|
||||
readonly VITE_AUTH_URL: string
|
||||
}
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
import { Button as Kobalte } from "@kobalte/core/button"
|
||||
import { JSX, Show, splitProps } from "solid-js"
|
||||
|
||||
export interface ButtonProps {
|
||||
color?: "primary" | "secondary" | "ghost"
|
||||
size?: "md" | "sm"
|
||||
icon?: JSX.Element
|
||||
}
|
||||
export function Button(props: JSX.IntrinsicElements["button"] & ButtonProps) {
|
||||
const [split, rest] = splitProps(props, ["color", "size", "icon"])
|
||||
return (
|
||||
<Kobalte
|
||||
{...rest}
|
||||
data-component="button"
|
||||
data-size={split.size || "md"}
|
||||
data-color={split.color || "primary"}
|
||||
>
|
||||
<Show when={props.icon}>
|
||||
<div data-slot="icon">{props.icon}</div>
|
||||
</Show>
|
||||
{props.children}
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
import { createContext, JSX, ParentProps, useContext } from "solid-js"
|
||||
import { StandardSchemaV1 } from "@standard-schema/spec"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { Dialog } from "./dialog"
|
||||
|
||||
const Context = createContext<DialogControl>()
|
||||
|
||||
type DialogControl = {
|
||||
open<Schema extends StandardSchemaV1<object>>(
|
||||
component: DialogComponent<Schema>,
|
||||
input: StandardSchemaV1.InferInput<Schema>,
|
||||
): void
|
||||
close(): void
|
||||
isOpen(input: any): boolean
|
||||
size: "sm" | "md"
|
||||
transition?: boolean
|
||||
input?: any
|
||||
}
|
||||
|
||||
type DialogProps<Schema extends StandardSchemaV1<object>> = {
|
||||
input: StandardSchemaV1.InferInput<Schema>
|
||||
control: DialogControl
|
||||
}
|
||||
|
||||
type DialogComponent<Schema extends StandardSchemaV1<object>> = ReturnType<
|
||||
typeof createDialog<Schema>
|
||||
>
|
||||
|
||||
export function createDialog<Schema extends StandardSchemaV1<object>>(props: {
|
||||
schema: Schema
|
||||
size: "sm" | "md"
|
||||
render: (props: DialogProps<Schema>) => JSX.Element
|
||||
}) {
|
||||
const result = () => {
|
||||
const dialog = useDialog()
|
||||
return (
|
||||
<Dialog
|
||||
size={dialog.size}
|
||||
transition={dialog.transition}
|
||||
open={dialog.isOpen(result)}
|
||||
onOpenChange={(val) => {
|
||||
if (!val) dialog.close()
|
||||
}}
|
||||
>
|
||||
{props.render({
|
||||
input: dialog.input,
|
||||
control: dialog,
|
||||
})}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
result.schema = props.schema
|
||||
result.size = props.size
|
||||
return result
|
||||
}
|
||||
|
||||
export function DialogProvider(props: ParentProps) {
|
||||
const [store, setStore] = createStore<{
|
||||
dialog?: DialogComponent<any>
|
||||
input?: any
|
||||
transition?: boolean
|
||||
size: "sm" | "md"
|
||||
}>({
|
||||
size: "sm",
|
||||
})
|
||||
|
||||
const control: DialogControl = {
|
||||
get input() {
|
||||
return store.input
|
||||
},
|
||||
get size() {
|
||||
return store.size
|
||||
},
|
||||
get transition() {
|
||||
return store.transition
|
||||
},
|
||||
isOpen(input) {
|
||||
return store.dialog === input
|
||||
},
|
||||
open(component, input) {
|
||||
setStore({
|
||||
dialog: component,
|
||||
input: input,
|
||||
size: store.dialog !== undefined ? store.size : component.size,
|
||||
transition: store.dialog !== undefined,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
setStore({
|
||||
size: component.size,
|
||||
})
|
||||
}, 0)
|
||||
|
||||
setTimeout(() => {
|
||||
setStore({
|
||||
transition: false,
|
||||
})
|
||||
}, 150)
|
||||
},
|
||||
close() {
|
||||
setStore({
|
||||
dialog: undefined,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Context.Provider value={control}>{props.children}</Context.Provider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function useDialog() {
|
||||
const ctx = useContext(Context)
|
||||
if (!ctx) {
|
||||
throw new Error("useDialog must be used within a DialogProvider")
|
||||
}
|
||||
return ctx
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
.options {
|
||||
margin-top: var(--space-1);
|
||||
border-top: 2px solid var(--color-border);
|
||||
padding: var(--space-2);
|
||||
|
||||
[data-slot="option"] {
|
||||
outline: none;
|
||||
flex-shrink: 0;
|
||||
height: var(--space-11);
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
padding: 0 var(--space-2-5);
|
||||
gap: var(--space-3);
|
||||
cursor: pointer;
|
||||
|
||||
&[data-empty] {
|
||||
cursor: default;
|
||||
color: var(--color-text-dimmed);
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
background-color: var(--color-bg-surface);
|
||||
}
|
||||
|
||||
[data-slot="title"] {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
[data-slot="prefix"] {
|
||||
width: var(--space-4);
|
||||
height: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
import style from "./dialog-select.module.css"
|
||||
import { z } from "zod"
|
||||
import { createMemo, createSignal, For, JSX, onMount } from "solid-js"
|
||||
import { createList } from "solid-list"
|
||||
import { createDialog } from "./context-dialog"
|
||||
|
||||
export const DialogSelect = createDialog({
|
||||
size: "md",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
placeholder: z.string(),
|
||||
onSelect: z
|
||||
.function(z.tuple([z.any()]))
|
||||
.returns(z.void())
|
||||
.optional(),
|
||||
options: z.array(
|
||||
z.object({
|
||||
display: z.string(),
|
||||
value: z.any().optional(),
|
||||
onSelect: z.function().returns(z.void()).optional(),
|
||||
prefix: z.custom<JSX.Element>().optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
render: (ctx) => {
|
||||
let input: HTMLInputElement
|
||||
onMount(() => {
|
||||
input.focus()
|
||||
input.value = ""
|
||||
})
|
||||
|
||||
const [filter, setFilter] = createSignal("")
|
||||
const filtered = createMemo(() =>
|
||||
ctx.input.options?.filter((i) =>
|
||||
i.display.toLowerCase().includes(filter().toLowerCase()),
|
||||
),
|
||||
)
|
||||
const list = createList({
|
||||
loop: true,
|
||||
initialActive: 0,
|
||||
items: () => filtered().map((_, i) => i),
|
||||
handleTab: false,
|
||||
})
|
||||
|
||||
const handleSelection = (index: number) => {
|
||||
const option = ctx.input.options[index]
|
||||
|
||||
// If the option has its own onSelect handler, use it
|
||||
if (option.onSelect) {
|
||||
option.onSelect()
|
||||
}
|
||||
// Otherwise, if there's a global onSelect handler, call it with the option's value
|
||||
else if (ctx.input.onSelect) {
|
||||
ctx.input.onSelect(
|
||||
option.value !== undefined ? option.value : option.display,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-slot="header">
|
||||
<label
|
||||
data-size="md"
|
||||
data-slot="title"
|
||||
data-component="label"
|
||||
for={`dialog-select-${ctx.input.title}`}
|
||||
>
|
||||
{ctx.input.title}
|
||||
</label>
|
||||
</div>
|
||||
<div data-slot="main">
|
||||
<input
|
||||
data-size="lg"
|
||||
data-component="input"
|
||||
value={filter()}
|
||||
onInput={(e) => {
|
||||
setFilter(e.target.value)
|
||||
list.setActive(0)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const selected = list.active()
|
||||
if (selected === null) return
|
||||
handleSelection(selected)
|
||||
return
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
setFilter("")
|
||||
return
|
||||
}
|
||||
list.onKeyDown(e)
|
||||
}}
|
||||
id={`dialog-select-${ctx.input.title}`}
|
||||
ref={(r) => (input = r)}
|
||||
data-slot="input"
|
||||
placeholder={ctx.input.placeholder}
|
||||
/>
|
||||
</div>
|
||||
<div data-slot="options" class={style.options}>
|
||||
<For
|
||||
each={filtered()}
|
||||
fallback={
|
||||
<div data-slot="option" data-empty>
|
||||
No results
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{(option, index) => (
|
||||
<div
|
||||
onClick={() => handleSelection(index())}
|
||||
data-slot="option"
|
||||
data-active={list.active() === index() ? true : undefined}
|
||||
>
|
||||
{option.prefix && <div data-slot="prefix">{option.prefix}</div>}
|
||||
<div data-slot="title">{option.display}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
})
|
|
@ -1,70 +0,0 @@
|
|||
import { z } from "zod"
|
||||
import { onMount } from "solid-js"
|
||||
import { createDialog } from "./context-dialog"
|
||||
import { Button } from "./button"
|
||||
|
||||
export const DialogString = createDialog({
|
||||
size: "sm",
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
placeholder: z.string(),
|
||||
action: z.string(),
|
||||
onSubmit: z.function().args(z.string()).returns(z.void()),
|
||||
}),
|
||||
render: (ctx) => {
|
||||
let input: HTMLInputElement
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
input.focus()
|
||||
input.value = ""
|
||||
}, 50)
|
||||
})
|
||||
|
||||
function submit() {
|
||||
const value = input.value.trim()
|
||||
if (value) {
|
||||
ctx.input.onSubmit(value)
|
||||
ctx.control.close()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-slot="header">
|
||||
<label
|
||||
data-size="md"
|
||||
data-slot="title"
|
||||
data-component="label"
|
||||
for={`dialog-string-${ctx.input.title}`}
|
||||
>
|
||||
{ctx.input.title}
|
||||
</label>
|
||||
</div>
|
||||
<div data-slot="main">
|
||||
<input
|
||||
data-slot="input"
|
||||
data-size="lg"
|
||||
data-component="input"
|
||||
ref={(r) => (input = r)}
|
||||
placeholder={ctx.input.placeholder}
|
||||
id={`dialog-string-${ctx.input.title}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault()
|
||||
submit()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div data-slot="footer">
|
||||
<Button size="md" color="ghost" onClick={() => ctx.control.close()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="md" color="secondary" onClick={submit}>
|
||||
{ctx.input.action}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
},
|
||||
})
|
|
@ -1,27 +0,0 @@
|
|||
import { Dialog as Kobalte } from "@kobalte/core/dialog"
|
||||
import { ComponentProps, ParentProps } from "solid-js"
|
||||
|
||||
export type Props = ParentProps<{
|
||||
size?: "sm" | "md"
|
||||
transition?: boolean
|
||||
}> &
|
||||
ComponentProps<typeof Kobalte>
|
||||
|
||||
export function Dialog(props: Props) {
|
||||
return (
|
||||
<Kobalte {...props}>
|
||||
<Kobalte.Portal>
|
||||
<Kobalte.Overlay data-component="dialog-overlay" />
|
||||
<div data-component="dialog-center">
|
||||
<Kobalte.Content
|
||||
data-transition={props.transition ? "" : undefined}
|
||||
data-size={props.size}
|
||||
data-slot="content"
|
||||
>
|
||||
{props.children}
|
||||
</Kobalte.Content>
|
||||
</div>
|
||||
</Kobalte.Portal>
|
||||
</Kobalte>
|
||||
)
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
[data-component="button"] {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
line-height: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--font-size-md);
|
||||
text-transform: uppercase;
|
||||
height: var(--space-11);
|
||||
outline: none;
|
||||
font-weight: 500;
|
||||
padding: 0 var(--space-4);
|
||||
border-width: 2px;
|
||||
border-color: var(--color-border);
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&[data-color="primary"] {
|
||||
background-color: var(--color-text);
|
||||
border-color: var(--color-text);
|
||||
color: var(--color-text-invert);
|
||||
|
||||
&:active {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-color="secondary"] {
|
||||
&:active {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-color="ghost"] {
|
||||
border: none;
|
||||
text-decoration: underline;
|
||||
|
||||
&:active {
|
||||
color: var(--color-text-accent);
|
||||
}
|
||||
}
|
||||
|
||||
&:has([data-slot="icon"]) {
|
||||
padding-left: var(--space-3);
|
||||
padding-right: var(--space-3);
|
||||
}
|
||||
|
||||
&[data-size="sm"] {
|
||||
height: var(--space-8);
|
||||
padding: var(--space-3);
|
||||
font-size: var(--font-size-xs);
|
||||
|
||||
[data-slot="icon"] {
|
||||
width: var(--space-3-5);
|
||||
height: var(--space-3-5);
|
||||
}
|
||||
|
||||
&:has([data-slot="icon"]) {
|
||||
padding-left: var(--space-2);
|
||||
padding-right: var(--space-2);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="icon"] {
|
||||
width: var(--space-4);
|
||||
height: var(--space-4);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&[data-rotate] [data-slot="icon"] {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
[data-component="dialog-overlay"] {
|
||||
pointer-events: none !important;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
animation-name: fadeOut;
|
||||
animation-duration: 200ms;
|
||||
animation-timing-function: ease;
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(2px);
|
||||
|
||||
&[data-expanded] {
|
||||
animation-name: fadeIn;
|
||||
opacity: 1;
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="dialog-center"] {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
padding-top: 10vh;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
|
||||
[data-slot="content"] {
|
||||
width: 45rem;
|
||||
margin: 0 auto;
|
||||
transition: 150ms width;
|
||||
background-color: var(--color-bg);
|
||||
border-width: 2px;
|
||||
border-color: var(--color-border);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
outline: none;
|
||||
animation-duration: 1ms;
|
||||
animation-name: zoomOut;
|
||||
animation-timing-function: ease;
|
||||
|
||||
box-shadow: 8px 8px 0px 0px var(--color-gray-4);
|
||||
|
||||
&[data-expanded] {
|
||||
animation-name: zoomIn;
|
||||
}
|
||||
|
||||
&[data-transition] {
|
||||
animation-duration: 200ms;
|
||||
}
|
||||
|
||||
&[data-size="sm"] {
|
||||
width: 30rem;
|
||||
}
|
||||
|
||||
[data-slot="header"] {
|
||||
display: flex;
|
||||
padding: var(--space-4) var(--space-4) 0;
|
||||
|
||||
[data-slot="title"] {
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="main"] {
|
||||
padding: 0 var(--space-4);
|
||||
|
||||
&:has([data-slot="options"]) {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="input"] {
|
||||
}
|
||||
|
||||
[data-slot="footer"] {
|
||||
padding: var(--space-4);
|
||||
display: flex;
|
||||
gap: var(--space-4);
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
[data-component="input"] {
|
||||
font-size: var(--font-size-md);
|
||||
background: transparent;
|
||||
caret-color: var(--color-accent);
|
||||
font-family: var(--font-mono);
|
||||
height: var(--space-11);
|
||||
padding: 0 var(--space-4);
|
||||
width: 100%;
|
||||
resize: none;
|
||||
border: 2px solid var(--color-border);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-dimmed);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&[data-size="sm"] {
|
||||
height: var(--space-9);
|
||||
padding: 0 var(--space-3);
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
&[data-size="md"] {
|
||||
}
|
||||
|
||||
&[data-size="lg"] {
|
||||
height: var(--space-12);
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
[data-component="label"] {
|
||||
letter-spacing: -0.03125rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-dimmed);
|
||||
font-weight: 500;
|
||||
font-size: var(--font-size-md);
|
||||
|
||||
&[data-size="sm"] {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
&[data-size="md"] {
|
||||
}
|
||||
&[data-size="lg"] {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
[data-component="title-bar"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 72px;
|
||||
padding: 0 var(--space-4);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
|
||||
[data-slot="left"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1-5);
|
||||
|
||||
h1 {
|
||||
letter-spacing: -0.03125rem;
|
||||
font-size: var(--font-size-xl);
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text-dimmed);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
[data-component="title-bar"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
/* tokens */
|
||||
@import "./token/color.css";
|
||||
@import "./token/reset.css";
|
||||
@import "./token/animation.css";
|
||||
@import "./token/font.css";
|
||||
@import "./token/space.css";
|
||||
|
||||
/* components */
|
||||
@import "./component/label.css";
|
||||
@import "./component/input.css";
|
||||
@import "./component/button.css";
|
||||
@import "./component/dialog.css";
|
||||
@import "./component/title-bar.css";
|
||||
|
||||
body {
|
||||
font-family: var(--font-mono);
|
||||
line-height: 1;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
text-underline-offset: 0.1875rem;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
&:active {
|
||||
color: var(--color-text-accent);
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--color-text-accent-invert);
|
||||
}
|
||||
|
||||
/* Responsive utilities */
|
||||
[data-max-width] {
|
||||
width: 100%;
|
||||
|
||||
& > * {
|
||||
max-width: 90rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&[data-max-width-64] > * {
|
||||
max-width: 64rem;
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
@keyframes zoomIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoomOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
:root {
|
||||
--color-white: hsl(0, 0%, 100%);
|
||||
--color-gray-1: hsl(224, 20%, 94%);
|
||||
--color-gray-2: hsl(224, 6%, 77%);
|
||||
--color-gray-3: hsl(224, 6%, 56%);
|
||||
--color-gray-4: hsl(224, 7%, 36%);
|
||||
--color-gray-5: hsl(224, 10%, 23%);
|
||||
--color-gray-6: hsl(224, 14%, 16%);
|
||||
--color-black: hsl(224, 10%, 10%);
|
||||
|
||||
--hue-orange: 41;
|
||||
--color-orange-low: hsl(var(--hue-orange), 39%, 22%);
|
||||
--color-orange: hsl(var(--hue-orange), 82%, 63%);
|
||||
--color-orange-high: hsl(var(--hue-orange), 82%, 87%);
|
||||
--hue-green: 101;
|
||||
--color-green-low: hsl(var(--hue-green), 39%, 22%);
|
||||
--color-green: hsl(var(--hue-green), 82%, 63%);
|
||||
--color-green-high: hsl(var(--hue-green), 82%, 80%);
|
||||
--hue-blue: 234;
|
||||
--color-blue-low: hsl(var(--hue-blue), 54%, 20%);
|
||||
--color-blue: hsl(var(--hue-blue), 100%, 60%);
|
||||
--color-blue-high: hsl(var(--hue-blue), 100%, 87%);
|
||||
--hue-purple: 281;
|
||||
--color-purple-low: hsl(var(--hue-purple), 39%, 22%);
|
||||
--color-purple: hsl(var(--hue-purple), 82%, 63%);
|
||||
--color-purple-high: hsl(var(--hue-purple), 82%, 89%);
|
||||
--hue-red: 339;
|
||||
--color-red-low: hsl(var(--hue-red), 39%, 22%);
|
||||
--color-red: hsl(var(--hue-red), 82%, 63%);
|
||||
--color-red-high: hsl(var(--hue-red), 82%, 87%);
|
||||
|
||||
--color-accent-low: hsl(13, 75%, 30%);
|
||||
--color-accent: hsl(13, 88%, 57%);
|
||||
--color-accent-high: hsl(13, 100%, 78%);
|
||||
|
||||
--color-text: var(--color-gray-1);
|
||||
--color-text-dimmed: var(--color-gray-3);
|
||||
--color-text-accent: var(--color-accent);
|
||||
--color-text-invert: var(--color-black);
|
||||
--color-text-accent-invert: var(--color-accent-high);
|
||||
--color-bg: var(--color-black);
|
||||
--color-bg-surface: var(--color-gray-5);
|
||||
--color-bg-accent: var(--color-accent-high);
|
||||
--color-border: var(--color-gray-2);
|
||||
|
||||
--color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);
|
||||
}
|
||||
|
||||
:root[data-color-mode="light"] {
|
||||
--color-white: hsl(224, 10%, 10%);
|
||||
--color-gray-1: hsl(224, 14%, 16%);
|
||||
--color-gray-2: hsl(224, 10%, 23%);
|
||||
--color-gray-3: hsl(224, 7%, 36%);
|
||||
--color-gray-4: hsl(224, 6%, 56%);
|
||||
--color-gray-5: hsl(224, 6%, 77%);
|
||||
--color-gray-6: hsl(224, 20%, 94%);
|
||||
--color-gray-7: hsl(224, 19%, 97%);
|
||||
--color-black: hsl(0, 0%, 100%);
|
||||
|
||||
--color-orange-high: hsl(var(--hue-orange), 80%, 25%);
|
||||
--color-orange: hsl(var(--hue-orange), 90%, 60%);
|
||||
--color-orange-low: hsl(var(--hue-orange), 90%, 88%);
|
||||
--color-green-high: hsl(var(--hue-green), 80%, 22%);
|
||||
--color-green: hsl(var(--hue-green), 90%, 46%);
|
||||
--color-green-low: hsl(var(--hue-green), 85%, 90%);
|
||||
--color-blue-high: hsl(var(--hue-blue), 80%, 30%);
|
||||
--color-blue: hsl(var(--hue-blue), 90%, 60%);
|
||||
--color-blue-low: hsl(var(--hue-blue), 88%, 90%);
|
||||
--color-purple-high: hsl(var(--hue-purple), 90%, 30%);
|
||||
--color-purple: hsl(var(--hue-purple), 90%, 60%);
|
||||
--color-purple-low: hsl(var(--hue-purple), 80%, 90%);
|
||||
--color-red-high: hsl(var(--hue-red), 80%, 30%);
|
||||
--color-red: hsl(var(--hue-red), 90%, 60%);
|
||||
--color-red-low: hsl(var(--hue-red), 80%, 90%);
|
||||
|
||||
--color-accent-high: hsl(13, 75%, 26%);
|
||||
--color-accent: hsl(13, 88%, 60%);
|
||||
--color-accent-low: hsl(13, 100%, 89%);
|
||||
|
||||
--color-text-accent: var(--color-accent);
|
||||
--color-text-dimmed: var(--color-gray-4);
|
||||
--color-text-invert: var(--color-black);
|
||||
--color-text-accent-invert: var(--color-accent-low);
|
||||
--color-bg-surface: var(--color-gray-6);
|
||||
--color-bg-accent: var(--color-accent);
|
||||
|
||||
--color-backdrop-overlay: hsla(225, 9%, 36%, 0.66);
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
:root {
|
||||
--font-size-2xs: 0.6875rem;
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.8125rem;
|
||||
--font-size-md: 0.9375rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
--font-size-4xl: 2.25rem;
|
||||
--font-size-5xl: 3rem;
|
||||
--font-size-6xl: 3.75rem;
|
||||
--font-size-7xl: 4.5rem;
|
||||
--font-size-8xl: 6rem;
|
||||
--font-size-9xl: 8rem;
|
||||
--font-mono: IBM Plex Mono, monospace;
|
||||
--font-sans: Rubik, sans-serif;
|
||||
|
||||
--font-line-height: 1.75;
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: var(--global-color-border, currentColor);
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: 1.5;
|
||||
--font-fallback: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
|
||||
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-moz-tab-size: 4;
|
||||
tab-size: 4;
|
||||
font-family: var(--global-font-body, var(--font-fallback));
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
color: inherit;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
border-color: inherit;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
color: var(--global-color-placeholder, #9ca3af);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
abbr:where([title]) {
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-size: 1em;
|
||||
--font-mono-fallback: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New";
|
||||
font-family: var(--global-font-mono, var(--font-fallback));
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="search"],
|
||||
input[type="password"] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
::-webkit-search-decoration,
|
||||
::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
:root {
|
||||
--space-0: 0;
|
||||
--space-px: 1px;
|
||||
--space-0-5: 0.125rem;
|
||||
--space-1: 0.25rem;
|
||||
--space-1-5: 0.375rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-2-5: 0.625rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-3-5: 0.875rem;
|
||||
--space-4: 1rem;
|
||||
--space-4-5: 1.125rem;
|
||||
--space-5: 1.25rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-7: 1.75rem;
|
||||
--space-8: 2rem;
|
||||
--space-9: 2.25rem;
|
||||
--space-10: 2.5rem;
|
||||
--space-11: 2.75rem;
|
||||
--space-12: 3rem;
|
||||
--space-14: 3.5rem;
|
||||
--space-16: 4rem;
|
||||
--space-20: 5rem;
|
||||
--space-24: 6rem;
|
||||
--space-28: 7rem;
|
||||
--space-32: 8rem;
|
||||
--space-36: 9rem;
|
||||
--space-40: 10rem;
|
||||
--space-44: 11rem;
|
||||
--space-48: 12rem;
|
||||
--space-52: 13rem;
|
||||
--space-56: 14rem;
|
||||
--space-60: 15rem;
|
||||
--space-64: 16rem;
|
||||
--space-72: 18rem;
|
||||
--space-80: 20rem;
|
||||
--space-96: 24rem;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue