pinokiod 7.3.10 → 7.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kernel/bin/brew.js +12 -2
- package/kernel/bin/caddy.js +24 -20
- package/kernel/bin/huggingface.js +2 -2
- package/kernel/bin/setup.js +2 -3
- package/kernel/bin/uv.js +13 -6
- package/kernel/connect/index.js +5 -1
- package/kernel/connect/providers/huggingface/index.js +213 -75
- package/kernel/environment.js +16 -1
- package/kernel/router/localhost_home_router.js +7 -0
- package/kernel/shell.js +1 -5
- package/kernel/util.js +1 -0
- package/package.json +1 -1
- package/server/index.js +75 -33
- package/server/public/common.js +45 -71
- package/server/public/install.js +20 -2
- package/server/public/layout.js +1 -1
- package/server/public/style.css +1432 -511
- package/server/public/tab-link-popover.css +162 -18
- package/server/public/tab-link-popover.js +230 -21
- package/server/public/task-launcher.css +121 -84
- package/server/public/terminal-settings.js +121 -45
- package/server/public/universal-launcher.css +42 -33
- package/server/public/urldropdown.css +284 -0
- package/server/views/app.ejs +1608 -321
- package/server/views/autolaunch.ejs +4 -5
- package/server/views/checkpoints.ejs +223 -50
- package/server/views/connect/huggingface.ejs +406 -325
- package/server/views/connect.ejs +0 -1
- package/server/views/github.ejs +277 -324
- package/server/views/index.ejs +53 -1
- package/server/views/install.ejs +119 -19
- package/server/views/logs.ejs +10 -9
- package/server/views/net.ejs +341 -64
- package/server/views/network.ejs +85 -63
- package/server/views/partials/main_sidebar.ejs +237 -12
- package/server/views/plugins.ejs +141 -3
- package/server/views/settings.ejs +103 -7
- package/server/views/setup.ejs +0 -5
- package/server/views/skills.ejs +0 -1
- package/server/views/task_list.ejs +0 -1
- package/server/views/terminal.ejs +142 -1
- package/server/views/terminals.ejs +346 -6
- package/server/views/tools.ejs +828 -1691
- package/test/caddy-install.test.js +53 -0
- package/test/connect-setup.test.js +16 -0
- package/test/github-connection.test.js +1 -1
- package/test/huggingface-bin.test.js +4 -4
- package/test/huggingface-connect.test.js +73 -0
- package/test/main-sidebar.test.js +31 -0
- package/test/shell-run-template.test.js +5 -1
- package/test/uv-bin.test.js +29 -0
|
@@ -1,350 +1,431 @@
|
|
|
1
|
-
<!
|
|
1
|
+
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Hugging Face Login</title>
|
|
7
|
+
<link href="/css/fontawesome.min.css" rel="stylesheet">
|
|
8
|
+
<link href="/css/solid.min.css" rel="stylesheet">
|
|
9
|
+
<link href="/css/regular.min.css" rel="stylesheet">
|
|
10
|
+
<link href="/css/brands.min.css" rel="stylesheet">
|
|
11
|
+
<link href="/noty.css" rel="stylesheet">
|
|
12
|
+
<link href="/style.css" rel="stylesheet">
|
|
13
|
+
<link href="/task-launcher.css" rel="stylesheet">
|
|
14
|
+
<% if (agent === "electron") { %>
|
|
15
|
+
<link href="/electron.css" rel="stylesheet"/>
|
|
16
|
+
<% } %>
|
|
17
|
+
<style>
|
|
18
|
+
.hf-login-shell {
|
|
19
|
+
max-width: 980px;
|
|
20
|
+
}
|
|
21
|
+
.hf-state-panel {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: flex-start;
|
|
24
|
+
gap: 14px;
|
|
25
|
+
}
|
|
26
|
+
.hf-state-icon {
|
|
27
|
+
width: 36px;
|
|
28
|
+
height: 36px;
|
|
29
|
+
border-radius: 8px;
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
flex: 0 0 auto;
|
|
34
|
+
background: var(--task-chip-bg, var(--task-soft));
|
|
35
|
+
border: 1px solid var(--task-border);
|
|
36
|
+
color: var(--task-text);
|
|
37
|
+
}
|
|
38
|
+
.hf-state-copy {
|
|
39
|
+
min-width: 0;
|
|
40
|
+
}
|
|
41
|
+
.hf-state-title {
|
|
42
|
+
margin: 0;
|
|
43
|
+
color: var(--task-text);
|
|
44
|
+
font-size: 18px;
|
|
45
|
+
font-weight: 800;
|
|
46
|
+
line-height: 1.25;
|
|
47
|
+
letter-spacing: 0;
|
|
48
|
+
}
|
|
49
|
+
.hf-state-detail {
|
|
50
|
+
margin: 8px 0 0;
|
|
51
|
+
color: var(--task-muted);
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
line-height: 1.55;
|
|
54
|
+
}
|
|
55
|
+
.hf-action-row {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-wrap: wrap;
|
|
58
|
+
gap: 10px;
|
|
59
|
+
margin-top: 20px;
|
|
60
|
+
}
|
|
61
|
+
.hf-code-panel {
|
|
62
|
+
display: grid;
|
|
63
|
+
gap: 10px;
|
|
64
|
+
max-width: 520px;
|
|
65
|
+
margin-top: 18px;
|
|
66
|
+
padding: 14px;
|
|
67
|
+
border: 1px solid var(--task-border);
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
background: var(--task-panel);
|
|
70
|
+
}
|
|
71
|
+
.hf-code-panel.hidden {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
.hf-code-label {
|
|
75
|
+
margin: 0;
|
|
76
|
+
color: var(--task-muted);
|
|
77
|
+
font-size: 13px;
|
|
78
|
+
line-height: 1.45;
|
|
79
|
+
}
|
|
80
|
+
.hf-code-row {
|
|
81
|
+
display: flex;
|
|
82
|
+
flex-wrap: wrap;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 10px;
|
|
85
|
+
}
|
|
86
|
+
.hf-code-value {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
padding: 9px 11px;
|
|
90
|
+
border: 1px solid var(--task-border);
|
|
91
|
+
border-radius: 6px;
|
|
92
|
+
color: var(--task-text);
|
|
93
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
94
|
+
font-size: 15px;
|
|
95
|
+
font-weight: 800;
|
|
96
|
+
letter-spacing: 0.08em;
|
|
97
|
+
}
|
|
98
|
+
.hf-error {
|
|
99
|
+
margin-top: 16px;
|
|
100
|
+
}
|
|
101
|
+
</style>
|
|
59
102
|
</head>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<div id="user-details"></div>
|
|
79
|
-
<button class="btn secondary" onclick="logout()">Logout</button>
|
|
103
|
+
<%
|
|
104
|
+
const selected = "connect";
|
|
105
|
+
%>
|
|
106
|
+
<body class="<%= theme %> main-sidebar-page task-launcher-page task-page setup-page" data-agent="<%= agent %>" data-page="huggingface-connect">
|
|
107
|
+
<%- include('../partials/app_navheader', { agent }) %>
|
|
108
|
+
<main>
|
|
109
|
+
<div class="task-container">
|
|
110
|
+
<section class="task-shell hf-login-shell">
|
|
111
|
+
<header class="task-shell-header">
|
|
112
|
+
<div class="task-shell-header-main">
|
|
113
|
+
<div class="task-shell-topline">
|
|
114
|
+
<p class="task-eyebrow">Login / Hugging Face</p>
|
|
115
|
+
</div>
|
|
116
|
+
<h1 class="task-title">Authorize Hugging Face</h1>
|
|
117
|
+
<p class="task-description">Copy the device code, open Hugging Face, then approve the login. Pinokio will continue when the token is saved.</p>
|
|
118
|
+
<div class="task-badge-row task-header-badges">
|
|
119
|
+
<span class="task-badge task-badge-accent" id="hf-badge">Starting</span>
|
|
120
|
+
<span class="task-badge task-badge-neutral">Managed by huggingface_hub</span>
|
|
80
121
|
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</header>
|
|
124
|
+
|
|
125
|
+
<div class="task-shell-body">
|
|
126
|
+
<section class="task-section task-section-subtle">
|
|
127
|
+
<div class="hf-state-panel">
|
|
128
|
+
<div class="hf-state-icon" aria-hidden="true">
|
|
129
|
+
<i class="fa-solid fa-circle-notch fa-spin" id="hf-icon"></i>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="hf-state-copy">
|
|
132
|
+
<h2 class="hf-state-title" id="hf-title">Checking Hugging Face login</h2>
|
|
133
|
+
<p class="hf-state-detail" id="hf-detail">Pinokio is checking whether a Hugging Face token is already saved.</p>
|
|
134
|
+
<div class="hf-action-row">
|
|
135
|
+
<button class="task-button primary hidden" id="hf-login" type="button">
|
|
136
|
+
<i class="fa-solid fa-key" aria-hidden="true"></i>
|
|
137
|
+
<span>Log In</span>
|
|
138
|
+
</button>
|
|
139
|
+
<a class="task-button hidden" id="hf-done" href="/connect">
|
|
140
|
+
<i class="fa-solid fa-check" aria-hidden="true"></i>
|
|
141
|
+
<span>Done</span>
|
|
142
|
+
</a>
|
|
143
|
+
<button class="task-button danger hidden" id="hf-logout" type="button">
|
|
144
|
+
<i class="fa-solid fa-circle-xmark" aria-hidden="true"></i>
|
|
145
|
+
<span>Logout</span>
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="hf-code-panel hidden" id="hf-code">
|
|
149
|
+
<p class="hf-code-label">Hugging Face is asking for this device code.</p>
|
|
150
|
+
<div class="hf-code-row">
|
|
151
|
+
<div class="hf-code-value" id="hf-code-value"></div>
|
|
152
|
+
<button class="task-button primary" id="hf-open" type="button">
|
|
153
|
+
<i class="fa-solid fa-arrow-up-right-from-square" aria-hidden="true"></i>
|
|
154
|
+
<span>Copy and Open Hugging Face</span>
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="task-status error hidden hf-error" id="hf-error"></div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</section>
|
|
81
162
|
</div>
|
|
163
|
+
</section>
|
|
82
164
|
</div>
|
|
165
|
+
<%- include('../partials/main_sidebar', { selected }) %>
|
|
166
|
+
</main>
|
|
83
167
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
168
|
+
<script>
|
|
169
|
+
const LOGIN_URL = "/connect/huggingface/login"
|
|
170
|
+
const LOGOUT_URL = "/connect/huggingface/logout"
|
|
171
|
+
const KEYS_URL = "/connect/huggingface/keys"
|
|
172
|
+
const PROFILE_URL = "/connect/huggingface/profile"
|
|
173
|
+
const POLL_INTERVAL = 2000
|
|
174
|
+
const POLL_TIMEOUT = 120000
|
|
91
175
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
176
|
+
const badgeElement = document.getElementById("hf-badge")
|
|
177
|
+
const iconElement = document.getElementById("hf-icon")
|
|
178
|
+
const titleElement = document.getElementById("hf-title")
|
|
179
|
+
const detailElement = document.getElementById("hf-detail")
|
|
180
|
+
const openButton = document.getElementById("hf-open")
|
|
181
|
+
const loginButton = document.getElementById("hf-login")
|
|
182
|
+
const doneLink = document.getElementById("hf-done")
|
|
183
|
+
const logoutButton = document.getElementById("hf-logout")
|
|
184
|
+
const codeElement = document.getElementById("hf-code")
|
|
185
|
+
const codeValueElement = document.getElementById("hf-code-value")
|
|
186
|
+
const errorElement = document.getElementById("hf-error")
|
|
98
187
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
for (let i = 0; i < length; i++) {
|
|
103
|
-
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
104
|
-
}
|
|
105
|
-
return result;
|
|
106
|
-
}
|
|
188
|
+
let loginUrl = null
|
|
189
|
+
let pollTimer = null
|
|
190
|
+
let timeoutTimer = null
|
|
107
191
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
192
|
+
function setState({ badge, icon, title, detail, done = false }) {
|
|
193
|
+
if (badge) badgeElement.textContent = badge
|
|
194
|
+
if (icon) iconElement.className = icon
|
|
195
|
+
if (title) titleElement.textContent = title
|
|
196
|
+
if (detail) detailElement.textContent = detail
|
|
197
|
+
doneLink.classList.toggle("hidden", !done)
|
|
198
|
+
}
|
|
114
199
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return base64URLEncode(new Uint8Array(digest));
|
|
120
|
-
}
|
|
200
|
+
function setLoginLabel(label) {
|
|
201
|
+
const labelElement = loginButton.querySelector("span")
|
|
202
|
+
if (labelElement) labelElement.textContent = label
|
|
203
|
+
}
|
|
121
204
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
205
|
+
function showError(message, options = {}) {
|
|
206
|
+
const allowLogin = options.allowLogin !== false
|
|
207
|
+
errorElement.textContent = message
|
|
208
|
+
errorElement.classList.remove("hidden")
|
|
209
|
+
setLoginLabel("Try Again")
|
|
210
|
+
loginButton.classList.toggle("hidden", !allowLogin)
|
|
211
|
+
logoutButton.classList.add("hidden")
|
|
212
|
+
setState({
|
|
213
|
+
badge: "Needs attention",
|
|
214
|
+
icon: "fa-solid fa-triangle-exclamation",
|
|
215
|
+
title: "Login needs attention",
|
|
216
|
+
detail: "Pinokio could not complete the Hugging Face browser login."
|
|
217
|
+
})
|
|
218
|
+
}
|
|
128
219
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
body: JSON.stringify({})
|
|
135
|
-
});
|
|
136
|
-
const json = await res.json();
|
|
137
|
-
console.log({ json })
|
|
138
|
-
if (json.error) {
|
|
139
|
-
return null
|
|
140
|
-
} else if (json.access_token) {
|
|
141
|
-
return json.access_token
|
|
142
|
-
} else {
|
|
143
|
-
return null
|
|
144
|
-
}
|
|
145
|
-
}
|
|
220
|
+
function clearError() {
|
|
221
|
+
errorElement.textContent = ""
|
|
222
|
+
errorElement.classList.add("hidden")
|
|
223
|
+
loginButton.classList.add("hidden")
|
|
224
|
+
}
|
|
146
225
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
158
|
-
|
|
159
|
-
// Store for later
|
|
160
|
-
localStorage.setItem('oauth_state', state);
|
|
161
|
-
localStorage.setItem('code_verifier', codeVerifier);
|
|
162
|
-
|
|
163
|
-
// Build auth URL
|
|
164
|
-
const params = new URLSearchParams({
|
|
165
|
-
client_id: CLIENT_ID,
|
|
166
|
-
redirect_uri: REDIRECT_URI,
|
|
167
|
-
response_type: 'code',
|
|
168
|
-
scope: 'openid profile email read-repos write-repos manage-repos',
|
|
169
|
-
state: state,
|
|
170
|
-
code_challenge: codeChallenge,
|
|
171
|
-
code_challenge_method: 'S256'
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const authUrl = HF_OAUTH_URL + '?' + params.toString();
|
|
175
|
-
|
|
176
|
-
// Redirect
|
|
177
|
-
window.location.href = authUrl;
|
|
178
|
-
|
|
179
|
-
} catch (error) {
|
|
180
|
-
console.error('Login error:', error);
|
|
181
|
-
setStatus('Failed to start login', 'error');
|
|
182
|
-
}
|
|
183
|
-
}
|
|
226
|
+
function stopPolling() {
|
|
227
|
+
if (pollTimer) {
|
|
228
|
+
clearInterval(pollTimer)
|
|
229
|
+
pollTimer = null
|
|
230
|
+
}
|
|
231
|
+
if (timeoutTimer) {
|
|
232
|
+
clearTimeout(timeoutTimer)
|
|
233
|
+
timeoutTimer = null
|
|
234
|
+
}
|
|
235
|
+
}
|
|
184
236
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// Exchange code for tokens
|
|
203
|
-
const response = await fetch(HF_TOKEN_URL, {
|
|
204
|
-
method: 'POST',
|
|
205
|
-
headers: {
|
|
206
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
207
|
-
},
|
|
208
|
-
body: new URLSearchParams({
|
|
209
|
-
client_id: CLIENT_ID,
|
|
210
|
-
code: code,
|
|
211
|
-
redirect_uri: REDIRECT_URI,
|
|
212
|
-
grant_type: 'authorization_code',
|
|
213
|
-
code_verifier: codeVerifier
|
|
214
|
-
})
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
if (!response.ok) {
|
|
218
|
-
const errorText = await response.text();
|
|
219
|
-
throw new Error(`Token exchange failed: ${response.status} - ${errorText}`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const tokenData = await response.json();
|
|
223
|
-
console.log('Token response:', tokenData);
|
|
224
|
-
|
|
225
|
-
// Store tokens
|
|
226
|
-
if (tokenData.access_token) {
|
|
237
|
+
function showLoggedOut(options = {}) {
|
|
238
|
+
stopPolling()
|
|
239
|
+
clearError()
|
|
240
|
+
loginUrl = null
|
|
241
|
+
codeElement.classList.add("hidden")
|
|
242
|
+
codeValueElement.textContent = ""
|
|
243
|
+
logoutButton.classList.add("hidden")
|
|
244
|
+
setLoginLabel(options.loginLabel || "Log In")
|
|
245
|
+
loginButton.classList.remove("hidden")
|
|
246
|
+
setState({
|
|
247
|
+
badge: options.badge || "Not connected",
|
|
248
|
+
icon: options.icon || "fa-regular fa-circle",
|
|
249
|
+
title: options.title || "Hugging Face not connected",
|
|
250
|
+
detail: options.detail || "Log in when you need Hugging Face access for gated models or private repositories.",
|
|
251
|
+
done: Boolean(options.done)
|
|
252
|
+
})
|
|
253
|
+
}
|
|
227
254
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
localStorage.removeItem('code_verifier');
|
|
237
|
-
window.history.replaceState({}, document.title, window.location.pathname);
|
|
238
|
-
|
|
239
|
-
// Get user info
|
|
240
|
-
await fetchUserInfo();
|
|
241
|
-
} else {
|
|
242
|
-
throw new Error('No access token received');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
} catch (error) {
|
|
246
|
-
console.error('OAuth callback error:', error);
|
|
247
|
-
setStatus('Authentication failed: ' + error.message, 'error');
|
|
248
|
-
}
|
|
249
|
-
}
|
|
255
|
+
async function postJSON(url, body = {}) {
|
|
256
|
+
const res = await fetch(url, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: { "Content-Type": "application/json" },
|
|
259
|
+
body: JSON.stringify(body)
|
|
260
|
+
})
|
|
261
|
+
return res.json()
|
|
262
|
+
}
|
|
250
263
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (!token) {
|
|
256
|
-
throw new Error('No valid token available');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
const response = await fetch(HF_API_URL, {
|
|
260
|
-
headers: { 'Authorization': 'Bearer ' + token }
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
if (!response.ok) {
|
|
264
|
-
throw new Error(`API call failed: ${response.status}`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const userInfo = await response.json();
|
|
268
|
-
displayUserInfo(userInfo);
|
|
269
|
-
|
|
270
|
-
} catch (error) {
|
|
271
|
-
console.error('Error fetching user info:', error);
|
|
272
|
-
setStatus('Failed to fetch user info: ' + error.message, 'error');
|
|
273
|
-
logout();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
264
|
+
async function readToken() {
|
|
265
|
+
const json = await postJSON(KEYS_URL)
|
|
266
|
+
return json && json.access_token && !json.error ? json.access_token : null
|
|
267
|
+
}
|
|
276
268
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
269
|
+
async function showConnected() {
|
|
270
|
+
stopPolling()
|
|
271
|
+
clearError()
|
|
272
|
+
codeElement.classList.add("hidden")
|
|
273
|
+
loginButton.classList.add("hidden")
|
|
274
|
+
logoutButton.classList.remove("hidden")
|
|
275
|
+
try {
|
|
276
|
+
const res = await fetch(PROFILE_URL)
|
|
277
|
+
const profile = await res.json()
|
|
278
|
+
const nameRow = profile && Array.isArray(profile.items) ? profile.items.find((item) => item.key === "username") : null
|
|
279
|
+
const username = nameRow && nameRow.val ? ` as ${nameRow.val}` : ""
|
|
280
|
+
setState({
|
|
281
|
+
badge: "Connected",
|
|
282
|
+
icon: "fa-solid fa-circle-check",
|
|
283
|
+
title: "Hugging Face connected",
|
|
284
|
+
detail: `Pinokio is signed in${username}.`,
|
|
285
|
+
done: true
|
|
286
|
+
})
|
|
287
|
+
} catch (_) {
|
|
288
|
+
setState({
|
|
289
|
+
badge: "Connected",
|
|
290
|
+
icon: "fa-solid fa-circle-check",
|
|
291
|
+
title: "Hugging Face connected",
|
|
292
|
+
detail: "Pinokio is signed in.",
|
|
293
|
+
done: true
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
}
|
|
290
297
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const json = await res.json();
|
|
301
|
-
location.href = location.href
|
|
298
|
+
async function logout() {
|
|
299
|
+
stopPolling()
|
|
300
|
+
clearError()
|
|
301
|
+
logoutButton.disabled = true
|
|
302
|
+
logoutButton.classList.add("is-busy")
|
|
303
|
+
try {
|
|
304
|
+
const response = await postJSON(LOGOUT_URL)
|
|
305
|
+
if (response.error) {
|
|
306
|
+
throw new Error(response.error)
|
|
302
307
|
}
|
|
308
|
+
loginUrl = null
|
|
309
|
+
codeElement.classList.add("hidden")
|
|
310
|
+
codeValueElement.textContent = ""
|
|
311
|
+
showLoggedOut({
|
|
312
|
+
badge: "Signed out",
|
|
313
|
+
icon: "fa-solid fa-circle-xmark",
|
|
314
|
+
title: "Hugging Face signed out",
|
|
315
|
+
detail: "Pinokio removed the saved Hugging Face token.",
|
|
316
|
+
done: true,
|
|
317
|
+
loginLabel: "Log In Again"
|
|
318
|
+
})
|
|
319
|
+
} catch (error) {
|
|
320
|
+
showError(error.message, { allowLogin: false })
|
|
321
|
+
logoutButton.classList.remove("hidden")
|
|
322
|
+
} finally {
|
|
323
|
+
logoutButton.disabled = false
|
|
324
|
+
logoutButton.classList.remove("is-busy")
|
|
325
|
+
}
|
|
326
|
+
}
|
|
303
327
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
328
|
+
async function pollLoginStatus() {
|
|
329
|
+
const token = await readToken()
|
|
330
|
+
if (token) {
|
|
331
|
+
await showConnected()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function startPolling() {
|
|
336
|
+
stopPolling()
|
|
337
|
+
pollLoginStatus()
|
|
338
|
+
pollTimer = setInterval(pollLoginStatus, POLL_INTERVAL)
|
|
339
|
+
timeoutTimer = setTimeout(() => {
|
|
340
|
+
stopPolling()
|
|
341
|
+
setLoginLabel("Try Again")
|
|
342
|
+
loginButton.classList.remove("hidden")
|
|
343
|
+
setState({
|
|
344
|
+
badge: "Still waiting",
|
|
345
|
+
icon: "fa-regular fa-clock",
|
|
346
|
+
title: "Still waiting for Hugging Face",
|
|
347
|
+
detail: "The browser login did not finish in time."
|
|
348
|
+
})
|
|
349
|
+
}, POLL_TIMEOUT)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function copyAndOpen() {
|
|
353
|
+
if (!loginUrl) return
|
|
354
|
+
const value = codeValueElement.textContent.trim()
|
|
355
|
+
if (value) {
|
|
356
|
+
try {
|
|
357
|
+
await navigator.clipboard.writeText(value)
|
|
358
|
+
} catch (_) {}
|
|
359
|
+
}
|
|
360
|
+
await fetch("/pinokio/open", {
|
|
361
|
+
method: "POST",
|
|
362
|
+
headers: { "Content-Type": "application/json" },
|
|
363
|
+
body: JSON.stringify({ url: loginUrl, surface: "browser" })
|
|
364
|
+
}).catch(() => {})
|
|
365
|
+
openButton.querySelector("span").textContent = "Copied and Opened"
|
|
366
|
+
setTimeout(() => {
|
|
367
|
+
openButton.querySelector("span").textContent = "Copy and Open Hugging Face"
|
|
368
|
+
}, 1500)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function startLogin() {
|
|
372
|
+
stopPolling()
|
|
373
|
+
clearError()
|
|
374
|
+
loginUrl = null
|
|
375
|
+
doneLink.classList.add("hidden")
|
|
376
|
+
logoutButton.classList.add("hidden")
|
|
377
|
+
loginButton.classList.add("hidden")
|
|
378
|
+
setLoginLabel("Try Again")
|
|
379
|
+
codeElement.classList.add("hidden")
|
|
380
|
+
codeValueElement.textContent = ""
|
|
381
|
+
setState({
|
|
382
|
+
badge: "Preparing",
|
|
383
|
+
icon: "fa-solid fa-circle-notch fa-spin",
|
|
384
|
+
title: "Preparing Hugging Face",
|
|
385
|
+
detail: "Pinokio is requesting a Hugging Face device code."
|
|
386
|
+
})
|
|
319
387
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (code) {
|
|
332
|
-
await handleOAuthCallback(code);
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Check existing session with automatic refresh
|
|
337
|
-
const token = await ensureValidToken();
|
|
338
|
-
if (token) {
|
|
339
|
-
await fetchUserInfo();
|
|
340
|
-
} else {
|
|
341
|
-
setStatus('Not authenticated', 'warning');
|
|
342
|
-
}
|
|
343
|
-
});
|
|
388
|
+
const response = await postJSON(LOGIN_URL)
|
|
389
|
+
if (response.error || response.status === "error") {
|
|
390
|
+
showError(response.error || "Unknown Hugging Face login error.")
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
if (!response.login || !response.login.verification_uri_complete) {
|
|
394
|
+
showError("Hugging Face did not return a browser login URL.")
|
|
395
|
+
return
|
|
396
|
+
}
|
|
344
397
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
398
|
+
loginUrl = response.login.verification_uri_complete
|
|
399
|
+
if (response.login.user_code) {
|
|
400
|
+
codeValueElement.textContent = response.login.user_code
|
|
401
|
+
codeElement.classList.remove("hidden")
|
|
402
|
+
}
|
|
403
|
+
setState({
|
|
404
|
+
badge: "Waiting",
|
|
405
|
+
icon: "fa-solid fa-circle-notch fa-spin",
|
|
406
|
+
title: "Authorize Hugging Face",
|
|
407
|
+
detail: "Use the button to copy the code and open Hugging Face. Keep this page open while Pinokio waits."
|
|
408
|
+
})
|
|
409
|
+
startPolling()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
openButton.addEventListener("click", copyAndOpen)
|
|
413
|
+
loginButton.addEventListener("click", startLogin)
|
|
414
|
+
logoutButton.addEventListener("click", logout)
|
|
415
|
+
|
|
416
|
+
window.addEventListener("load", async () => {
|
|
417
|
+
try {
|
|
418
|
+
const token = await readToken()
|
|
419
|
+
if (token) {
|
|
420
|
+
await showConnected()
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
showLoggedOut()
|
|
424
|
+
} catch (error) {
|
|
425
|
+
showError(error.message)
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
</script>
|
|
429
|
+
<%- include('../partials/app_common_scripts') %>
|
|
349
430
|
</body>
|
|
350
431
|
</html>
|