summaryrefslogtreecommitdiffhomepage
path: root/cloud/app/src
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-09-11 18:20:22 -0400
committerDax Raad <[email protected]>2025-09-11 18:20:37 -0400
commit79c73267cf83b9d96af50ac60cbda948c26d8a49 (patch)
tree1123736f726e1cdc629b797dd7e36d2d13e1c9b0 /cloud/app/src
parent54f7fb5019cc46e8eef758959ca474556cc98c1c (diff)
downloadopencode-79c73267cf83b9d96af50ac60cbda948c26d8a49.tar.gz
opencode-79c73267cf83b9d96af50ac60cbda948c26d8a49.zip
wip: zen
Diffstat (limited to 'cloud/app/src')
-rw-r--r--cloud/app/src/routes/workspace/[id].tsx476
1 files changed, 187 insertions, 289 deletions
diff --git a/cloud/app/src/routes/workspace/[id].tsx b/cloud/app/src/routes/workspace/[id].tsx
index b8f355507..222e51a15 100644
--- a/cloud/app/src/routes/workspace/[id].tsx
+++ b/cloud/app/src/routes/workspace/[id].tsx
@@ -2,9 +2,10 @@ import "./[id].css"
import { Billing } from "@opencode/cloud-core/billing.js"
import { Key } from "@opencode/cloud-core/key.js"
import { json, query, action, useParams, useAction, createAsync, useSubmission } from "@solidjs/router"
-import { createMemo, createSignal, For, Show } from "solid-js"
+import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
import { withActor } from "~/context/auth.withActor"
import { IconCopy, IconCheck } from "~/component/icon"
+import { createStore } from "solid-js/store"
function formatDateForTable(date: Date) {
const options: Intl.DateTimeFormatOptions = {
@@ -41,16 +42,24 @@ const listKeys = query(async (workspaceID: string) => {
return withActor(() => Key.list(), workspaceID)
}, "key.list")
-const createKey = action(async (workspaceID: string, name: string) => {
+const createKey = action(async (form: FormData) => {
"use server"
+ const name = form.get("name")?.toString().trim()
+ if (!name) return { error: "Name is required" }
+ const workspaceID = form.get("workspaceID")?.toString()
+ if (!workspaceID) return { error: "Workspace ID is required" }
return json(
withActor(() => Key.create({ name }), workspaceID),
{ revalidate: listKeys.key },
)
}, "key.create")
-const removeKey = action(async (workspaceID: string, id: string) => {
+const removeKey = action(async (form: FormData) => {
"use server"
+ const id = form.get("id")?.toString()
+ if (!id) return { error: "ID is required" }
+ const workspaceID = form.get("workspaceID")?.toString()
+ if (!workspaceID) return { error: "Workspace ID is required" }
return json(
withActor(() => Key.remove({ id }), workspaceID),
{ revalidate: listKeys.key },
@@ -92,127 +101,22 @@ const createCheckoutUrl = action(async (workspaceID: string, successUrl: string,
// return withActor(() => Billing.generatePortalUrl({ returnUrl }), workspaceID)
// }, "portalUrl")
-function KeysSection() {
- // Dummy data for testing
- const dummyKeys = [
- {
- id: "key_1",
- name: "Development API Key",
- key: "oc_dev_1234567890abcdef1234567890abcdef12345678",
- timeCreated: new Date("2024-01-15T10:30:00Z"),
- },
- {
- id: "key_2",
- name: "Production API Key",
- key: "oc_prod_abcdef1234567890abcdef1234567890abcdef12",
- timeCreated: new Date("2024-02-01T14:22:00Z"),
- },
- {
- id: "key_3",
- name: "Testing Environment",
- key: "oc_test_9876543210fedcba9876543210fedcba98765432",
- timeCreated: new Date("2024-02-10T09:15:00Z"),
- },
- ]
-
+function KeySection() {
const params = useParams()
const keys = createAsync(() => listKeys(params.id))
- // const keys = () => dummyKeys
- const [showForm, setShowForm] = createSignal(false)
- const [name, setName] = createSignal("")
- const removeAction = useAction(removeKey)
- const createAction = useAction(createKey)
- const createSubmission = useSubmission(createKey)
- const [copiedId, setCopiedId] = createSignal<string | null>(null)
function formatKey(key: string) {
if (key.length <= 11) return key
return `${key.slice(0, 7)}...${key.slice(-4)}`
}
- async function handleCreateKey() {
- if (!name().trim()) return
-
- try {
- await createAction(params.id, name().trim())
- setName("")
- setShowForm(false)
- } catch (error) {
- console.error("Failed to create API key:", error)
- }
- }
-
- async function copyKeyToClipboard(text: string, keyId: string) {
- try {
- await navigator.clipboard.writeText(text)
- setCopiedId(keyId)
- setTimeout(() => setCopiedId(null), 1500)
- } catch (error) {
- console.error("Failed to copy to clipboard:", error)
- }
- }
-
- async function handleDeleteKey(keyId: string) {
- if (!confirm("Are you sure you want to delete this API key?")) {
- return
- }
-
- try {
- await removeAction(params.id, keyId)
- } catch (error) {
- console.error("Failed to delete API key:", error)
- }
- }
-
return (
<section data-component="api-keys-section">
<div data-slot="section-title">
<h2>API Keys</h2>
<p>Manage your API keys for accessing opencode services.</p>
</div>
- <Show
- when={!showForm()}
- fallback={
- <div data-slot="create-form">
- <input
- data-component="input"
- type="text"
- placeholder="Enter key name"
- value={name()}
- onInput={(e) => setName(e.currentTarget.value)}
- onKeyPress={(e) => e.key === "Enter" && handleCreateKey()}
- />
- <div data-slot="form-actions">
- <button
- data-color="ghost"
- onClick={() => {
- setShowForm(false)
- setName("")
- }}
- >
- Cancel
- </button>
- <button
- data-color="primary"
- disabled={createSubmission.pending || !name().trim()}
- onClick={handleCreateKey}
- >
- {createSubmission.pending ? "Creating..." : "Create"}
- </button>
- </div>
- </div>
- }
- >
- <button
- data-color="primary"
- onClick={() => {
- console.log("clicked")
- setShowForm(true)
- }}
- >
- Create API Key
- </button>
- </Show>
+ <KeyCreateForm />
<div data-slot="api-keys-table">
<Show
when={keys()?.length}
@@ -233,35 +137,42 @@ function KeysSection() {
</thead>
<tbody>
<For each={keys()!}>
- {(key) => (
- <tr>
- <td data-slot="key-name">{key.name}</td>
- <td data-slot="key-value">
- <button
- data-color="ghost"
- disabled={copiedId() === key.id}
- onClick={() => copyKeyToClipboard(key.key, key.id)}
- title="Copy API key"
- >
- <span>{formatKey(key.key)}</span>
- <Show
- when={copiedId() === key.id}
- fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}
+ {(key) => {
+ const [copied, setCopied] = createSignal(false)
+ // const submission = useSubmission(removeKey, ([fd]) => fd.get("id")?.toString() === key.id)
+ return (
+ <tr>
+ <td data-slot="key-name">{key.name}</td>
+ <td data-slot="key-value">
+ <button
+ data-color="ghost"
+ disabled={copied()}
+ onClick={async () => {
+ await navigator.clipboard.writeText(key.key)
+ setCopied(true)
+ setTimeout(() => setCopied(false), 1000)
+ }}
+ title="Copy API key"
>
- <IconCheck style={{ width: "14px", height: "14px" }} />
- </Show>
- </button>
- </td>
- <td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
- {formatDateForTable(key.timeCreated)}
- </td>
- <td data-slot="key-actions">
- <button data-color="ghost" onClick={() => handleDeleteKey(key.id)} title="Delete API key">
- Delete
- </button>
- </td>
- </tr>
- )}
+ <span>{formatKey(key.key)}</span>
+ <Show when={copied()} fallback={<IconCopy style={{ width: "14px", height: "14px" }} />}>
+ <IconCheck style={{ width: "14px", height: "14px" }} />
+ </Show>
+ </button>
+ </td>
+ <td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
+ {formatDateForTable(key.timeCreated)}
+ </td>
+ <td data-slot="key-actions">
+ <form action={removeKey} method="post">
+ <input type="hidden" name="id" value={key.id} />
+ <input type="hidden" name="workspaceID" value={params.id} />
+ <button data-color="ghost">Delete</button>
+ </form>
+ </td>
+ </tr>
+ )
+ }}
</For>
</tbody>
</table>
@@ -271,28 +182,62 @@ function KeysSection() {
)
}
-function BalanceSection() {
+function KeyCreateForm() {
const params = useParams()
- const dummyBalanceInfo = { balance: 2500000000 } // $25.00 in cents
+ const submission = useSubmission(createKey)
+ const [store, setStore] = createStore({
+ show: false,
+ })
- const balanceInfo = createAsync(() => getBalanceInfo(params.id))
- // const balanceInfo = () => dummyBalanceInfo
- const createCheckoutUrlAction = useAction(createCheckoutUrl)
- const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
+ let input: HTMLInputElement
- async function handleBuyCredits() {
- try {
- const baseUrl = window.location.href
- const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
- if (checkoutUrl) {
- window.location.href = checkoutUrl
- }
- } catch (error) {
- console.error("Failed to get checkout URL:", error)
+ createEffect(() => {
+ if (!submission.pending && submission.result) {
+ hide()
}
+ })
+
+ function show() {
+ setStore("show", true)
+ input.focus()
+ }
+
+ function hide() {
+ setStore("show", false)
}
return (
+ <Show
+ when={store.show}
+ fallback={
+ <button data-color="primary" onClick={() => show()}>
+ Create API Key
+ </button>
+ }
+ >
+ <form action={createKey} method="post" data-slot="create-form">
+ <input ref={(r) => (input = r)} data-component="input" name="name" type="text" placeholder="Enter key name" />
+ <input type="hidden" name="workspaceID" value={params.id} />
+ <div data-slot="form-actions">
+ <button type="reset" data-color="ghost" onClick={() => hide()}>
+ Cancel
+ </button>
+ <button type="submit" data-color="primary" disabled={submission.pending}>
+ {submission.pending ? "Creating..." : "Create"}
+ </button>
+ </div>
+ </form>
+ </Show>
+ )
+}
+
+function BalanceSection() {
+ const params = useParams()
+ const balanceInfo = createAsync(() => getBalanceInfo(params.id))
+ const createCheckoutUrlAction = useAction(createCheckoutUrl)
+ const createCheckoutUrlSubmission = useSubmission(createCheckoutUrl)
+
+ return (
<section data-component="balance-section">
<div data-slot="section-title">
<h2>Balance</h2>
@@ -314,7 +259,17 @@ function BalanceSection() {
})()}
</span>
</div>
- <button data-color="primary" disabled={createCheckoutUrlSubmission.pending} onClick={handleBuyCredits}>
+ <button
+ data-color="primary"
+ disabled={createCheckoutUrlSubmission.pending}
+ onClick={async () => {
+ const baseUrl = window.location.href
+ const checkoutUrl = await createCheckoutUrlAction(params.id, baseUrl, baseUrl)
+ if (checkoutUrl) {
+ window.location.href = checkoutUrl
+ }
+ }}
+ >
{createCheckoutUrlSubmission.pending ? "Loading..." : "Buy Credits"}
</button>
</div>
@@ -324,43 +279,8 @@ function BalanceSection() {
function UsageSection() {
const params = useParams()
- const dummyUsage = [
- {
- id: "usage_1",
- model: "claude-3-sonnet-20240229",
- inputTokens: 1250,
- outputTokens: 890,
- cost: 125000000, // $1.25 in cents
- timeCreated: "2024-02-10T15:30:00Z",
- },
- {
- id: "usage_2",
- model: "gpt-4-turbo-preview",
- inputTokens: 2100,
- outputTokens: 1456,
- cost: 340000000, // $3.40 in cents
- timeCreated: "2024-02-09T09:45:00Z",
- },
- {
- id: "usage_3",
- model: "claude-3-haiku-20240307",
- inputTokens: 850,
- outputTokens: 620,
- cost: 45000000, // $0.45 in cents
- timeCreated: "2024-02-08T13:22:00Z",
- },
- {
- id: "usage_4",
- model: "gpt-3.5-turbo",
- inputTokens: 1800,
- outputTokens: 1200,
- cost: 85000000, // $0.85 in cents
- timeCreated: "2024-02-07T11:15:00Z",
- },
- ]
-
const usage = createAsync(() => getUsageInfo(params.id))
- // const usage = () => dummyUsage
+
return (
<section data-component="usage-section">
<div data-slot="section-title">
@@ -411,23 +331,9 @@ function UsageSection() {
)
}
-function PaymentsSection() {
+function PaymentSection() {
const params = useParams()
- const dummyPayments = [
- {
- id: "pi_1234567890",
- amount: 5000000000, // $50.00 in cents
- timeCreated: "2024-02-01T10:00:00Z",
- },
- {
- id: "pi_0987654321",
- amount: 2500000000, // $25.00 in cents
- timeCreated: "2024-01-15T14:30:00Z",
- },
- ]
-
const payments = createAsync(() => getPaymentsInfo(params.id))
- // const payments = () => dummyPayments
return (
payments() &&
@@ -471,97 +377,90 @@ function PaymentsSection() {
function NewUserSection() {
const params = useParams()
- const keys = createAsync(() => listKeys(params.id))
const [copiedKey, setCopiedKey] = createSignal(false)
-
- async function copyKeyToClipboard(text: string) {
- try {
- await navigator.clipboard.writeText(text)
- setCopiedKey(true)
- setTimeout(() => setCopiedKey(false), 2000)
- } catch (error) {
- console.error("Failed to copy to clipboard:", error)
- }
- }
+ const keys = createAsync(() => listKeys(params.id))
+ const usage = createAsync(() => getUsageInfo(params.id))
+ const isNew = createMemo(() => {
+ const keysList = keys()
+ const usageList = usage()
+ return keysList?.length === 1 && (!usageList || usageList.length === 0)
+ })
+ const defaultKey = createMemo(() => keys()?.at(-1)?.key)
return (
- <div data-slot="new-user-sections">
- <div data-component="feature-grid">
- <div data-slot="feature">
- <h3>Tested & Verified Models</h3>
- <p>We've benchmarked and tested models specifically for coding agents to ensure the best performance.</p>
- </div>
- <div data-slot="feature">
- <h3>Highest Quality</h3>
- <p>Access models configured for optimal performance - no downgrades or routing to cheaper providers.</p>
- </div>
- <div data-slot="feature">
- <h3>No Lock-in</h3>
- <p>Use Zen with any coding agent, and continue using other providers with opencode whenever you want.</p>
+ <Show when={isNew()}>
+ <div data-slot="new-user-sections">
+ <div data-component="feature-grid">
+ <div data-slot="feature">
+ <h3>Tested & Verified Models</h3>
+ <p>We've benchmarked and tested models specifically for coding agents to ensure the best performance.</p>
+ </div>
+ <div data-slot="feature">
+ <h3>Highest Quality</h3>
+ <p>Access models configured for optimal performance - no downgrades or routing to cheaper providers.</p>
+ </div>
+ <div data-slot="feature">
+ <h3>No Lock-in</h3>
+ <p>Use Zen with any coding agent, and continue using other providers with opencode whenever you want.</p>
+ </div>
</div>
- </div>
- <div data-component="api-key-highlight">
- <div data-slot="section-title">
- <h2>Your API Key</h2>
- </div>
+ <div data-component="api-key-highlight">
+ <div data-slot="section-title">
+ <h2>Your API Key</h2>
+ </div>
- <Show when={keys()?.length}>
- <div data-slot="key-display">
- <div data-slot="key-container">
- <code data-slot="key-value">{keys()![0].key}</code>
- <button
- data-color="primary"
- disabled={copiedKey()}
- onClick={() => copyKeyToClipboard(keys()![0].key)}
- title="Copy API key"
- >
- <Show
- when={copiedKey()}
- fallback={
- <>
- <IconCopy style={{ width: "16px", height: "16px" }} /> Copy Key
- </>
- }
+ <Show when={defaultKey()}>
+ <div data-slot="key-display">
+ <div data-slot="key-container">
+ <code data-slot="key-value">{defaultKey()}</code>
+ <button
+ data-color="primary"
+ disabled={copiedKey()}
+ onClick={async () => {
+ await navigator.clipboard.writeText(defaultKey() ?? "")
+ setCopiedKey(true)
+ setTimeout(() => setCopiedKey(false), 2000)
+ }}
+ title="Copy API key"
>
- <IconCheck style={{ width: "16px", height: "16px" }} /> Copied!
- </Show>
- </button>
+ <Show
+ when={copiedKey()}
+ fallback={
+ <>
+ <IconCopy style={{ width: "16px", height: "16px" }} /> Copy Key
+ </>
+ }
+ >
+ <IconCheck style={{ width: "16px", height: "16px" }} /> Copied!
+ </Show>
+ </button>
+ </div>
</div>
- </div>
- </Show>
- </div>
+ </Show>
+ </div>
- <div data-component="next-steps">
- <div data-slot="section-title">
- <h2>Next Steps</h2>
+ <div data-component="next-steps">
+ <div data-slot="section-title">
+ <h2>Next Steps</h2>
+ </div>
+ <ol>
+ <li>Copy your API key above</li>
+ <li>
+ Run <code>opencode auth login</code> and select opencode
+ </li>
+ <li>Paste your API key when prompted</li>
+ <li>
+ Run <code>/models</code> to see available models
+ </li>
+ </ol>
</div>
- <ol>
- <li>Copy your API key above</li>
- <li>
- Run <code>opencode auth login</code> and select opencode
- </li>
- <li>Paste your API key when prompted</li>
- <li>
- Run <code>/models</code> to see available models
- </li>
- </ol>
</div>
- </div>
+ </Show>
)
}
export default function () {
- const params = useParams()
- const keys = createAsync(() => listKeys(params.id))
- const usage = createAsync(() => getUsageInfo(params.id))
-
- const isNewUser = createMemo(() => {
- const keysList = keys()
- const usageList = usage()
- return keysList?.length === 1 && (!usageList || usageList.length === 0)
- })
-
return (
<div data-page="workspace-[id]">
<section data-component="title-section">
@@ -575,14 +474,13 @@ export default function () {
</p>
</section>
- <Show when={!isNewUser()} fallback={<NewUserSection />}>
- <div data-slot="sections">
- <KeysSection />
- <BalanceSection />
- <UsageSection />
- <PaymentsSection />
- </div>
- </Show>
+ <div data-slot="sections">
+ <NewUserSection />
+ <KeySection />
+ <BalanceSection />
+ <UsageSection />
+ <PaymentSection />
+ </div>
</div>
)
}