Jay V 5 ماه پیش
والد
کامیت
c2fa28c1be

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

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

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

@@ -47,12 +47,6 @@
             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) {
@@ -192,11 +186,35 @@
         &[data-slot="key-value"] {
           font-family: var(--font-mono);
 
-          div {
-            cursor: pointer;
+          button {
             display: flex;
             align-items: center;
             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"] {
             color: var(--color-danger);
           }
+          [data-slot="currency"] {
+            color: var(--color-danger);
+          }
         }
 
         [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>
                     <td data-slot="key-name">{key.name}</td>
                     <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>
                         <Show
                           when={copiedId() === key.id}
@@ -245,7 +250,7 @@ function KeysSection() {
                         >
                           <IconCheck style={{ width: "14px", height: "14px" }} />
                         </Show>
-                      </div>
+                      </button>
                     </td>
                     <td data-slot="key-date" title={formatDateUTC(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 (
     <div data-page="workspace-[id]">
       <section data-component="title-section">
@@ -478,12 +575,14 @@ export default function () {
         </p>
       </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>
   )
 }

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

@@ -79,7 +79,7 @@ $ opencode auth login
 ┌  Add credential
 ◆  Select provider
-│  ● Anthropic (recommended)
+│  ● Anthropic
 │  ○ OpenAI
 │  ○ Google
 │  ○ 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.
-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.
 
 It works like any other provider in opencode. And is completely optional to use
@@ -131,9 +131,7 @@ $ opencode auth login
 ┌  Add credential
 ◆  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.
 
 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.
 
 You are charged per request and you can add credits to your account.