Skip to content

Commit 4da64ee

Browse files
committed
User brings his own token for ai
1 parent a4cb4e7 commit 4da64ee

9 files changed

Lines changed: 346 additions & 58 deletions

File tree

angular.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
{
9191
"type": "anyComponentStyle",
9292
"maximumWarning": "4kb",
93-
"maximumError": "8kb"
93+
"maximumError": "12kb"
9494
}
9595
],
9696
"fileReplacements": [

bun.lock

Lines changed: 22 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openworkers-dash",
3-
"version": "1.2.2",
3+
"version": "1.2.3",
44
"license": "MIT",
55
"scripts": {
66
"ng": "ng",
@@ -14,34 +14,34 @@
1414
},
1515
"private": true,
1616
"dependencies": {
17-
"@angular/animations": "^21.0.0",
18-
"@angular/common": "^21.0.0",
19-
"@angular/compiler": "^21.0.0",
20-
"@angular/core": "^21.0.0",
21-
"@angular/forms": "^21.0.0",
22-
"@angular/platform-browser": "^21.0.0",
23-
"@angular/platform-browser-dynamic": "^21.0.0",
24-
"@angular/router": "^21.0.0",
17+
"@angular/animations": "^21.0.7",
18+
"@angular/common": "^21.0.7",
19+
"@angular/compiler": "^21.0.7",
20+
"@angular/core": "^21.0.7",
21+
"@angular/forms": "^21.0.7",
22+
"@angular/platform-browser": "^21.0.7",
23+
"@angular/platform-browser-dynamic": "^21.0.7",
24+
"@angular/router": "^21.0.7",
2525
"@materia-ui/ngx-monaco-editor": "^6.0.0",
2626
"@ng-icons/core": "^33.0.0",
2727
"@ng-icons/heroicons": "^33.0.0",
28-
"@openworkers/croner-wasm": "^0.3.0",
28+
"@openworkers/croner-wasm": "^0.3.1",
2929
"dexie": "^4.2.1",
3030
"monaco-editor": "0.55.1",
31-
"rxjs": "^7.8.1",
32-
"tslib": "^2.3.0"
31+
"rxjs": "^7.8.2",
32+
"tslib": "^2.8.1"
3333
},
3434
"devDependencies": {
35-
"@angular/build": "^21.0.0",
36-
"@angular/cli": "~21.0.0",
37-
"@angular/compiler-cli": "^21.0.0",
35+
"@angular/build": "^21.0.5",
36+
"@angular/cli": "~21.0.5",
37+
"@angular/compiler-cli": "^21.0.7",
3838
"@openworkers/api-types": "1.2.1",
3939
"@openworkers/workers-types": "^0.1.6",
4040
"@tailwindcss/postcss": "4.1.17",
4141
"@types/jasmine": "5.1.5",
42-
"ansi_up": "^6.0.2",
43-
"autoprefixer": "^10.4.13",
44-
"postcss": "^8.4.20",
42+
"ansi_up": "^6.0.6",
43+
"autoprefixer": "^10.4.23",
44+
"postcss": "^8.5.6",
4545
"tailwindcss": "4.1.17",
4646
"typescript": "5.9.3"
4747
}

src/app/modules/account/account.page.html

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,85 @@ <h4>API Keys</h4>
143143
</div>
144144
</div>
145145

146+
<!-- Claude Token Card -->
147+
<div class="card shadow-sm mt-6">
148+
<div class="p-4">
149+
<h4 class="mb-2">Claude AI</h4>
150+
<p class="text-sm text-gray-500 mb-4">
151+
Use your own Claude API credentials for the AI assistant.
152+
<br><span class="text-xs italic">Token stored locally in your browser, proxied through our servers but never persisted. <a href="https://github.com/openworkers/openworkers-api/blob/main/src/routes/ai.ts" target="_blank" class="text-blue hover:underline">Source code</a>.</span>
153+
</p>
154+
155+
<!-- Token types table -->
156+
<div class="overflow-x-auto mb-4">
157+
<table class="w-full text-xs">
158+
<thead>
159+
<tr class="text-left text-gray-500 border-b dark:border-gray-700">
160+
<th class="pb-2 pr-2">Type</th>
161+
<th class="pb-2 pr-2">Prefix</th>
162+
<th class="pb-2 pr-2">Source</th>
163+
<th class="pb-2">Notes</th>
164+
</tr>
165+
</thead>
166+
<tbody class="text-gray-600 dark:text-gray-400">
167+
<tr class="border-b dark:border-gray-700">
168+
<td class="py-2 pr-2 font-medium text-green-600 dark:text-green-400">API Key</td>
169+
<td class="py-2 pr-2 font-mono">sk-ant-api...</td>
170+
<td class="py-2 pr-2"><a href="https://console.anthropic.com/settings/keys" target="_blank" class="text-blue hover:underline">Anthropic Console</a></td>
171+
<td class="py-2">✅ Recommended. Pay-as-you-go billing. Never expires.</td>
172+
</tr>
173+
<tr class="border-b dark:border-gray-700">
174+
<td class="py-2 pr-2 font-medium text-yellow-600 dark:text-yellow-400">Access Token</td>
175+
<td class="py-2 pr-2 font-mono">sk-ant-oat...</td>
176+
<td class="py-2 pr-2"><a href="https://gist.github.com/max-lt/9a500c715f891574aa44652fe2a8ddf9" target="_blank" class="text-blue hover:underline">OAuth script</a></td>
177+
<td class="py-2">⚠️ Hacky. Uses Claude Pro/Max. Expires in ~8h, survives server restart.</td>
178+
</tr>
179+
<tr>
180+
<td class="py-2 pr-2 font-medium text-yellow-600 dark:text-yellow-400">Refresh Token</td>
181+
<td class="py-2 pr-2 font-mono">sk-ant-ort...</td>
182+
<td class="py-2 pr-2"><a href="https://gist.github.com/max-lt/9a500c715f891574aa44652fe2a8ddf9" target="_blank" class="text-blue hover:underline">OAuth script</a></td>
183+
<td class="py-2">⚠️ Hacky. Uses Claude Pro/Max. Auto-refreshes but invalidated on server restart.</td>
184+
</tr>
185+
</tbody>
186+
</table>
187+
</div>
188+
189+
<p class="text-xs text-gray-400 mb-4">
190+
<strong>Note:</strong> OAuth tokens (refresh/access) exploit Claude Code's OAuth flow to use your Claude Pro/Max subscription.
191+
This is unofficial and may break at any time. Refresh tokens rotate on each use — we keep track in memory, but a server restart will require you to generate a new token.
192+
</p>
193+
194+
@if (claudeTokenSaved$ | async) {
195+
<div class="flex items-center gap-3">
196+
<div class="flex-1 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-3 rounded">
197+
<span class="text-green-700 dark:text-green-300 text-sm">Token saved</span>
198+
</div>
199+
<button class="btn-outline text-sm" (click)="clearClaudeToken()">Clear</button>
200+
</div>
201+
} @else {
202+
<div class="flex gap-2">
203+
<input
204+
type="password"
205+
[(ngModel)]="claudeToken"
206+
placeholder="Paste your Claude token here"
207+
class="input-outline flex-1 font-mono text-sm"
208+
[disabled]="!!(claudeTokenTesting$ | async)"
209+
/>
210+
<button class="btn-blue" [disabled]="!claudeToken || !!(claudeTokenTesting$ | async)" (click)="saveClaudeToken()">
211+
@if (claudeTokenTesting$ | async) {
212+
Testing...
213+
} @else {
214+
Save
215+
}
216+
</button>
217+
</div>
218+
@if (claudeTokenError$ | async; as error) {
219+
<div class="mt-2 text-sm text-red">{{ error }}</div>
220+
}
221+
}
222+
</div>
223+
</div>
224+
146225
<!-- Logout -->
147226
<div class="mt-8">
148227
<button class="btn-outline" (click)="logout()">Logout</button>

src/app/modules/account/account.page.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import { CommonModule } from '@angular/common';
22
import { Component, OnInit } from '@angular/core';
33
import { FormsModule } from '@angular/forms';
44
import { Router } from '@angular/router';
5-
import { BehaviorSubject, Observable } from 'rxjs';
5+
import { BehaviorSubject, Observable, catchError, of } from 'rxjs';
66
import { AuthService } from '~/services/auth.service';
77
import { ApiKeysService } from '~/services/api-keys.service';
8+
import { AiService } from '~/services/ai.service';
89
import type { ISelf, IApiKey } from '@openworkers/api-types';
910

11+
const CLAUDE_TOKEN_KEY = 'claude_token';
12+
1013
@Component({
1114
standalone: true,
1215
imports: [CommonModule, FormsModule],
@@ -21,13 +24,21 @@ export default class AccountPage implements OnInit {
2124
newKeyName = '';
2225
newKeyToken: string | null = null;
2326

27+
// Claude token
28+
claudeToken = '';
29+
claudeTokenSaved$ = new BehaviorSubject<boolean>(false);
30+
claudeTokenTesting$ = new BehaviorSubject<boolean>(false);
31+
claudeTokenError$ = new BehaviorSubject<string>('');
32+
2433
constructor(
2534
private router: Router,
2635
private auth: AuthService,
27-
private apiKeys: ApiKeysService
36+
private apiKeys: ApiKeysService,
37+
private ai: AiService
2838
) {
2939
this.user$ = auth.user$;
3040
this.keys$ = apiKeys.keys$;
41+
this.loadClaudeToken();
3142
}
3243

3344
ngOnInit() {
@@ -68,6 +79,48 @@ export default class AccountPage implements OnInit {
6879
}
6980
}
7081

82+
// Claude token methods
83+
private loadClaudeToken() {
84+
const token = localStorage.getItem(CLAUDE_TOKEN_KEY);
85+
86+
if (token) {
87+
this.claudeToken = token;
88+
this.claudeTokenSaved$.next(true);
89+
}
90+
}
91+
92+
saveClaudeToken() {
93+
if (!this.claudeToken) return;
94+
95+
this.claudeTokenTesting$.next(true);
96+
this.claudeTokenError$.next('');
97+
98+
this.ai
99+
.testToken(this.claudeToken)
100+
.pipe(
101+
catchError((err) => {
102+
const error = err?.error?.error || 'Invalid token';
103+
return of({ valid: false, error });
104+
})
105+
)
106+
.subscribe((result) => {
107+
this.claudeTokenTesting$.next(false);
108+
109+
if (result.valid) {
110+
localStorage.setItem(CLAUDE_TOKEN_KEY, this.claudeToken);
111+
this.claudeTokenSaved$.next(true);
112+
} else {
113+
this.claudeTokenError$.next(result.error || 'Invalid token');
114+
}
115+
});
116+
}
117+
118+
clearClaudeToken() {
119+
localStorage.removeItem(CLAUDE_TOKEN_KEY);
120+
this.claudeToken = '';
121+
this.claudeTokenSaved$.next(false);
122+
}
123+
71124
logout() {
72125
this.auth.logout();
73126
this.router.navigate(['/sign-in']);

src/app/modules/worker/pages/worker-edit/components/ai-chat/ai-chat.component.css

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,12 +518,60 @@
518518
display: none;
519519
}
520520

521+
/* Toolbar with model selector and usage */
522+
.cli-toolbar {
523+
display: flex;
524+
justify-content: space-between;
525+
align-items: center;
526+
padding: 6px 0;
527+
gap: 12px;
528+
}
529+
530+
/* Model selector */
531+
.cli-model-selector {
532+
display: flex;
533+
gap: 2px;
534+
background: var(--bg-secondary);
535+
border-radius: 4px;
536+
padding: 2px;
537+
}
538+
539+
.cli-model-option {
540+
display: flex;
541+
align-items: center;
542+
cursor: pointer;
543+
}
544+
545+
.cli-model-option input {
546+
display: none;
547+
}
548+
549+
.cli-model-option span {
550+
padding: 4px 10px;
551+
font-size: 11px;
552+
border-radius: 3px;
553+
color: var(--text-muted);
554+
transition: all 0.15s;
555+
}
556+
557+
.cli-model-option:hover span {
558+
color: var(--text);
559+
}
560+
561+
.cli-model-option input:checked + span {
562+
background: var(--accent);
563+
color: white;
564+
}
565+
566+
.cli-model-option input:disabled + span {
567+
opacity: 0.5;
568+
cursor: not-allowed;
569+
}
570+
521571
/* Usage display */
522572
.cli-usage {
523573
display: flex;
524-
justify-content: flex-end;
525574
gap: 6px;
526-
padding: 4px 0;
527575
font-size: 11px;
528576
color: var(--text-muted);
529577
}
@@ -567,3 +615,30 @@
567615
opacity: 0.3;
568616
cursor: not-allowed;
569617
}
618+
619+
/* Coming soon section */
620+
.cli-coming-soon {
621+
margin-top: 20px;
622+
padding-top: 16px;
623+
border-top: 1px dashed var(--border);
624+
}
625+
626+
.cli-coming-soon-label {
627+
font-size: 11px;
628+
text-transform: uppercase;
629+
letter-spacing: 0.5px;
630+
color: var(--text-muted);
631+
margin-bottom: 10px;
632+
opacity: 0.7;
633+
}
634+
635+
.cli-hint-disabled {
636+
opacity: 0.4;
637+
cursor: not-allowed;
638+
border-style: dashed;
639+
}
640+
641+
.cli-hint-disabled:hover {
642+
border-color: var(--border);
643+
color: var(--text-hint);
644+
}

0 commit comments

Comments
 (0)