Jay V 5 месяцев назад
Родитель
Сommit
c2fa28c1be

+ 4 - 10
cloud/app/src/routes/workspace.css

@@ -15,7 +15,7 @@
     cursor: pointer;
     cursor: pointer;
     transition: all 0.15s ease;
     transition: all 0.15s ease;
 
 
-    &:hover {
+    &:hover:not(:disabled) {
       background-color: var(--color-surface-hover);
       background-color: var(--color-surface-hover);
       border-color: var(--color-accent);
       border-color: var(--color-accent);
     }
     }
@@ -26,13 +26,7 @@
 
 
     &:disabled {
     &:disabled {
       opacity: 0.5;
       opacity: 0.5;
-      cursor: not-allowed;
-
-      &:hover {
-        background-color: var(--color-bg);
-        border-color: var(--color-border);
-        transform: none;
-      }
+      transform: none;
     }
     }
 
 
     &[data-color="primary"] {
     &[data-color="primary"] {
@@ -40,7 +34,7 @@
       border-color: var(--color-primary);
       border-color: var(--color-primary);
       color: var(--color-primary-text);
       color: var(--color-primary-text);
 
 
-      &:hover {
+      &:hover:not(:disabled) {
         background-color: var(--color-primary-hover);
         background-color: var(--color-primary-hover);
         border-color: var(--color-primary-hover);
         border-color: var(--color-primary-hover);
       }
       }
@@ -51,7 +45,7 @@
       border-color: transparent;
       border-color: transparent;
       color: var(--color-text-muted);
       color: var(--color-text-muted);
 
 
-      &:hover {
+      &:hover:not(:disabled) {
         background-color: var(--color-surface-hover);
         background-color: var(--color-surface-hover);
         border-color: var(--color-border);
         border-color: var(--color-border);
         color: var(--color-text);
         color: var(--color-text);

+ 211 - 8
cloud/app/src/routes/workspace/[id].css

@@ -47,12 +47,6 @@
             font-size: var(--font-size-md);
             font-size: var(--font-size-md);
           }
           }
         }
         }
-
-        p {
-          line-height: 1.4;
-          font-size: var(--font-size-sm);
-          color: var(--color-text-muted);
-        }
       }
       }
     }
     }
     section:not(:last-child) {
     section:not(:last-child) {
@@ -192,11 +186,35 @@
         &[data-slot="key-value"] {
         &[data-slot="key-value"] {
           font-family: var(--font-mono);
           font-family: var(--font-mono);
 
 
-          div {
-            cursor: pointer;
+          button {
             display: flex;
             display: flex;
             align-items: center;
             align-items: center;
             gap: var(--space-2);
             gap: var(--space-2);
+            padding: var(--space-2) var(--space-3);
+            font-size: var(--font-size-sm);
+            font-weight: 400;
+            border: none;
+            background-color: transparent;
+            color: var(--color-text-muted);
+            font-family: var(--font-mono);
+            border-radius: var(--border-radius-sm);
+            cursor: pointer;
+            transition: all 0.15s ease;
+            text-transform: none;
+
+            &:hover:not(:disabled) {
+              background-color: var(--color-bg-surface);
+              color: var(--color-text);
+            }
+
+            &:disabled {
+              cursor: default;
+              color: var(--color-text);
+            }
+
+            span {
+              font-family: inherit;
+            }
           }
           }
         }
         }
 
 
@@ -262,6 +280,9 @@
           [data-slot="value"] {
           [data-slot="value"] {
             color: var(--color-danger);
             color: var(--color-danger);
           }
           }
+          [data-slot="currency"] {
+            color: var(--color-danger);
+          }
         }
         }
 
 
         [data-slot="currency"] {
         [data-slot="currency"] {
@@ -428,4 +449,186 @@
       }
       }
     }
     }
   }
   }
+
+  [data-slot="new-user-sections"] {
+    display: flex;
+    flex-direction: column;
+    gap: var(--space-16);
+
+    @media (max-width: 30rem) {
+      gap: var(--space-8);
+    }
+
+    [data-component="feature-grid"] {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+      gap: var(--space-6);
+
+      @media (max-width: 30rem) {
+        grid-template-columns: 1fr;
+        gap: var(--space-4);
+      }
+
+      [data-slot="feature"] {
+        display: flex;
+        flex-direction: column;
+        gap: var(--space-2);
+        padding: var(--space-4);
+        border: 1px solid var(--color-border);
+        border-radius: var(--border-radius-sm);
+        background-color: var(--color-bg-surface);
+
+        h3 {
+          font-size: var(--font-size-sm);
+          font-weight: 600;
+          margin: 0;
+          color: var(--color-text);
+          text-transform: uppercase;
+          letter-spacing: -0.025rem;
+        }
+
+        p {
+          font-size: var(--font-size-sm);
+          line-height: 1.5;
+          margin: 0;
+          color: var(--color-text-muted);
+        }
+      }
+    }
+
+    [data-component="api-key-highlight"] {
+      display: flex;
+      flex-direction: column;
+      gap: var(--space-6);
+
+      [data-slot="section-title"] {
+        display: flex;
+        flex-direction: column;
+        gap: var(--space-1);
+
+        h2 {
+          font-size: var(--font-size-md);
+          font-weight: 600;
+          line-height: 1.2;
+          letter-spacing: -0.03125rem;
+          margin: 0;
+          color: var(--color-text-secondary);
+          text-transform: uppercase;
+
+          @media (max-width: 30rem) {
+            font-size: var(--font-size-md);
+          }
+        }
+      }
+
+      [data-slot="key-display"] {
+        display: flex;
+        flex-direction: column;
+        gap: var(--space-3);
+
+        [data-slot="key-container"] {
+          display: flex;
+          gap: var(--space-3);
+          padding: var(--space-4);
+          border: 2px solid var(--color-accent);
+          border-radius: var(--border-radius-sm);
+          background-color: var(--color-bg-surface);
+          align-items: center;
+
+          @media (max-width: 40rem) {
+            flex-direction: column;
+            gap: var(--space-3);
+            align-items: stretch;
+          }
+
+          [data-slot="key-value"] {
+            flex: 1;
+            font-family: var(--font-mono);
+            font-size: var(--font-size-sm);
+            color: var(--color-text);
+            background-color: var(--color-bg);
+            padding: var(--space-3);
+            border-radius: var(--border-radius-sm);
+            border: 1px solid var(--color-border);
+            word-break: break-all;
+            line-height: 1.4;
+
+            @media (max-width: 40rem) {
+              font-size: var(--font-size-xs);
+              padding: var(--space-2-5);
+            }
+          }
+
+          button {
+            display: flex;
+            align-items: center;
+            gap: var(--space-2);
+            padding: var(--space-3) var(--space-4);
+            font-size: var(--font-size-sm);
+            font-weight: 500;
+            white-space: nowrap;
+            min-width: 130px;
+
+            @media (max-width: 40rem) {
+              justify-content: center;
+              padding: var(--space-2-5) var(--space-3);
+              font-size: var(--font-size-xs);
+              min-width: 96px;
+            }
+          }
+        }
+      }
+    }
+
+    [data-component="next-steps"] {
+      display: flex;
+      flex-direction: column;
+      gap: var(--space-6);
+
+      [data-slot="section-title"] {
+        display: flex;
+        flex-direction: column;
+        gap: var(--space-1);
+
+        h2 {
+          font-size: var(--font-size-md);
+          font-weight: 600;
+          line-height: 1.2;
+          letter-spacing: -0.03125rem;
+          margin: 0;
+          color: var(--color-text-secondary);
+          text-transform: uppercase;
+
+          @media (max-width: 30rem) {
+            font-size: var(--font-size-md);
+          }
+        }
+      }
+
+      ol {
+        margin: 0;
+        padding-left: 0;
+        display: flex;
+        flex-direction: column;
+        gap: var(--space-2);
+        list-style-position: inside;
+
+        li {
+          font-size: var(--font-size-sm);
+          line-height: 1.5;
+          color: var(--color-text-muted);
+
+          code {
+            font-family: var(--font-mono);
+            font-size: var(--font-size-xs);
+            padding: var(--space-1) var(--space-2);
+            background-color: var(--color-bg-surface);
+            border: 1px solid var(--color-border);
+            border-radius: var(--border-radius-sm);
+            color: var(--color-text);
+          }
+        }
+      }
+    }
+  }
 }
 }

+ 108 - 9
cloud/app/src/routes/workspace/[id].tsx

@@ -237,7 +237,12 @@ function KeysSection() {
                   <tr>
                   <tr>
                     <td data-slot="key-name">{key.name}</td>
                     <td data-slot="key-name">{key.name}</td>
                     <td data-slot="key-value">
                     <td data-slot="key-value">
-                      <div onClick={() => copyKeyToClipboard(key.key, key.id)} title="Click to copy API key">
+                      <button
+                        data-color="ghost"
+                        disabled={copiedId() === key.id}
+                        onClick={() => copyKeyToClipboard(key.key, key.id)}
+                        title="Copy API key"
+                      >
                         <span>{formatKey(key.key)}</span>
                         <span>{formatKey(key.key)}</span>
                         <Show
                         <Show
                           when={copiedId() === key.id}
                           when={copiedId() === key.id}
@@ -245,7 +250,7 @@ function KeysSection() {
                         >
                         >
                           <IconCheck style={{ width: "14px", height: "14px" }} />
                           <IconCheck style={{ width: "14px", height: "14px" }} />
                         </Show>
                         </Show>
-                      </div>
+                      </button>
                     </td>
                     </td>
                     <td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
                     <td data-slot="key-date" title={formatDateUTC(key.timeCreated)}>
                       {formatDateForTable(key.timeCreated)}
                       {formatDateForTable(key.timeCreated)}
@@ -464,7 +469,99 @@ function PaymentsSection() {
   )
   )
 }
 }
 
 
-export default function () {
+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)
+    }
+  }
+
+  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>
+        </div>
+      </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
+                    </>
+                  }
+                >
+                  <IconCheck style={{ width: "16px", height: "16px" }} /> Copied!
+                </Show>
+              </button>
+            </div>
+          </div>
+        </Show>
+      </div>
+
+      <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>
+    </div>
+  )
+}
+
+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 (
   return (
     <div data-page="workspace-[id]">
     <div data-page="workspace-[id]">
       <section data-component="title-section">
       <section data-component="title-section">
@@ -478,12 +575,14 @@ export default function () {
         </p>
         </p>
       </section>
       </section>
 
 
-      <div data-slot="sections">
-        <KeysSection />
-        <BalanceSection />
-        <UsageSection />
-        <PaymentsSection />
-      </div>
+      <Show when={!isNewUser()} fallback={<NewUserSection />}>
+        <div data-slot="sections">
+          <KeysSection />
+          <BalanceSection />
+          <UsageSection />
+          <PaymentsSection />
+        </div>
+      </Show>
     </div>
     </div>
   )
   )
 }
 }

+ 1 - 1
packages/web/src/content/docs/index.mdx

@@ -79,7 +79,7 @@ $ opencode auth login
 ┌  Add credential
 ┌  Add credential
 ◆  Select provider
 ◆  Select provider
-│  ● Anthropic (recommended)
+│  ● Anthropic
 │  ○ OpenAI
 │  ○ OpenAI
 │  ○ Google
 │  ○ Google
 │  ○ Amazon Bedrock
 │  ○ Amazon Bedrock

+ 2 - 4
packages/web/src/content/docs/providers.mdx

@@ -58,7 +58,7 @@ If you are new, we recommend starting with opencode zen.
 :::
 :::
 
 
 1. You sign in to **<a href={console}>opencode zen</a>** and get your API key.
 1. You sign in to **<a href={console}>opencode zen</a>** and get your API key.
-2. You run `opencode auth login` and select opencode zen and add your API key.
+2. You run `opencode auth login` and select opencode and add your API key.
 3. Run `/models` in the TUI to see the list of models we recommend.
 3. Run `/models` in the TUI to see the list of models we recommend.
 
 
 It works like any other provider in opencode. And is completely optional to use
 It works like any other provider in opencode. And is completely optional to use
@@ -131,9 +131,7 @@ $ opencode auth login
 ┌  Add credential
 ┌  Add credential
 ◆  Select provider
 ◆  Select provider
-│  ● Anthropic (recommended)
-│  ○ OpenAI
-│  ○ Google
+│  ● Anthropic
 │  ...
 │  ...
 ```
 ```

+ 1 - 1
packages/web/src/content/docs/zen.mdx

@@ -50,7 +50,7 @@ opencode zen is an AI gateway that gives you access to these models.
 opencode zen works like any other provider in opencode.
 opencode zen works like any other provider in opencode.
 
 
 1. You sign in to **<a href={console}>opencode zen</a>** and get your API key.
 1. You sign in to **<a href={console}>opencode zen</a>** and get your API key.
-2. You run `opencode auth login` and select opencode zen and add your API key.
+2. You run `opencode auth login` and select opencode and add your API key.
 3. Run `/models` in the TUI to see the list of models we recommend.
 3. Run `/models` in the TUI to see the list of models we recommend.
 
 
 You are charged per request and you can add credits to your account.
 You are charged per request and you can add credits to your account.