Paramjit Singh commited on
Commit
f815bcd
·
unverified ·
2 Parent(s): 42c15f3b632b90

Merge pull request #236 from Yuvraj-Sarathe/Hugging-Face-Token-Input

Browse files
.gitignore CHANGED
@@ -29,3 +29,4 @@ Thumbs.db
29
  # Misc
30
  *.log
31
  static/
 
 
29
  # Misc
30
  *.log
31
  static/
32
+ .planning/
frontend/src/components/auth/HuggingFaceTokenModal.tsx ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect, isValidElement, type ReactNode } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Input } from "@/components/ui/input";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ DialogTrigger,
14
+ } from "@/components/ui/dialog";
15
+ import { useAuthStore } from "@/store/auth-store";
16
+ import { Eye, EyeOff, AlertCircle, CheckCircle2, Loader2, ExternalLink, Key } from "lucide-react";
17
+
18
+ interface HuggingFaceTokenModalProps {
19
+ /** Optional — if provided, allows a button-triggered dialog pattern */
20
+ children?: ReactNode;
21
+ }
22
+
23
+ export default function HuggingFaceTokenModal({ children }: HuggingFaceTokenModalProps) {
24
+ const user = useAuthStore((state) => state.user);
25
+ const setHfToken = useAuthStore((state) => state.setHfToken);
26
+
27
+ const existingToken = user?.hf_token ?? "";
28
+ const hasExistingToken = existingToken.length > 0;
29
+
30
+ const [open, setOpen] = useState(false);
31
+ const [inputToken, setInputToken] = useState(existingToken);
32
+ const [saving, setSaving] = useState(false);
33
+ const [error, setError] = useState<string | null>(null);
34
+ const [success, setSuccess] = useState(false);
35
+ const [showToken, setShowToken] = useState(false);
36
+
37
+ const mountedRef = useRef(true);
38
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
39
+
40
+ // Cleanup auto-close timeout and unmount guard on unmount
41
+ useEffect(() => {
42
+ return () => {
43
+ mountedRef.current = false;
44
+ if (timeoutRef.current) {
45
+ clearTimeout(timeoutRef.current);
46
+ timeoutRef.current = null;
47
+ }
48
+ };
49
+ }, []);
50
+
51
+ const clearAutoCloseTimeout = () => {
52
+ if (timeoutRef.current) {
53
+ clearTimeout(timeoutRef.current);
54
+ timeoutRef.current = null;
55
+ }
56
+ };
57
+
58
+ const handleOpenChange = (newOpen: boolean) => {
59
+ clearAutoCloseTimeout();
60
+ setOpen(newOpen);
61
+ if (newOpen) {
62
+ // Reset to current store value when opening (picks up changes from background saves)
63
+ const currentToken = useAuthStore.getState().user?.hf_token ?? "";
64
+ setInputToken(currentToken);
65
+ setSaving(false);
66
+ setError(null);
67
+ setSuccess(false);
68
+ setShowToken(false);
69
+ }
70
+ };
71
+
72
+ const handleSave = async () => {
73
+ if (saving) return;
74
+ const token = inputToken.trim();
75
+ if (!token) {
76
+ setError("Please enter a valid token");
77
+ return;
78
+ }
79
+
80
+ setSaving(true);
81
+ setError(null);
82
+ setSuccess(false);
83
+
84
+ try {
85
+ await setHfToken(token);
86
+ if (!mountedRef.current) return;
87
+ setSaving(false);
88
+ setSuccess(true);
89
+ // Auto-close after 1.5s
90
+ timeoutRef.current = setTimeout(() => setOpen(false), 1500);
91
+ } catch (err) {
92
+ if (!mountedRef.current) return;
93
+ setSaving(false);
94
+ setError(err instanceof Error ? err.message : "Failed to save token");
95
+ }
96
+ };
97
+
98
+ const isSaveDisabled = inputToken.trim() === "" || saving;
99
+
100
+ return (
101
+ <Dialog open={open} onOpenChange={handleOpenChange}>
102
+ {children ? (
103
+ <DialogTrigger render={isValidElement(children) ? children : <span>{children}</span>} />
104
+ ) : (
105
+ <DialogTrigger
106
+ render={
107
+ <button className="flex w-full cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground">
108
+ <Key className="mr-2 h-4 w-4" />
109
+ <span>HuggingFace Token</span>
110
+ </button>
111
+ }
112
+ />
113
+ )}
114
+ <DialogContent className="max-w-md sm:rounded-2xl border-border/40 p-6 md:p-8 bg-background/95 backdrop-blur-xl shadow-2xl" showCloseButton={false}>
115
+ <DialogHeader className="gap-1">
116
+ <DialogTitle className="text-2xl font-bold tracking-tight">
117
+ 🤗 HuggingFace Token
118
+ </DialogTitle>
119
+ <DialogDescription className="text-sm text-muted-foreground mt-1.5">
120
+ Enter your HuggingFace API token to enable inference endpoints and model access.
121
+ </DialogDescription>
122
+ </DialogHeader>
123
+
124
+ <form onSubmit={(e) => { e.preventDefault(); if (!isSaveDisabled) handleSave(); }}>
125
+ <div className="space-y-4 mt-6">
126
+ {/* Token label with configured indicator */}
127
+ <div className="flex items-center gap-2">
128
+ <label htmlFor="hf-token-input" className="text-sm font-medium text-foreground/80">
129
+ Token
130
+ </label>
131
+ {hasExistingToken && (
132
+ <span className="inline-flex items-center gap-1 text-xs text-primary">
133
+ <CheckCircle2 className="w-3 h-3" />
134
+ Token configured
135
+ </span>
136
+ )}
137
+ </div>
138
+
139
+ {/* Input wrapper with visibility toggle */}
140
+ <div className="relative">
141
+ <Input
142
+ id="hf-token-input"
143
+ type={showToken ? "text" : "password"}
144
+ value={inputToken}
145
+ onChange={(e) => {
146
+ setInputToken(e.target.value);
147
+ if (error) setError(null);
148
+ if (success) setSuccess(false);
149
+ }}
150
+ placeholder="hf_..."
151
+ className="pr-10 font-mono"
152
+ disabled={saving}
153
+ autoFocus
154
+ aria-label="HuggingFace API Token"
155
+ />
156
+ <Button
157
+ variant="ghost"
158
+ size="icon-xs"
159
+ className="absolute right-2 top-1/2 -translate-y-1/2"
160
+ onClick={() => setShowToken(!showToken)}
161
+ type="button"
162
+ aria-label={showToken ? "Hide token" : "Show token"}
163
+ disabled={saving}
164
+ >
165
+ {showToken ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
166
+ </Button>
167
+ </div>
168
+
169
+ {/* External link */}
170
+ <a
171
+ href="https://huggingface.co/settings/tokens"
172
+ target="_blank"
173
+ rel="noopener noreferrer"
174
+ className="text-xs text-muted-foreground hover:text-primary underline-offset-2 transition-colors inline-flex items-center gap-1"
175
+ >
176
+ <ExternalLink className="w-3 h-3" />
177
+ Get your API token from HuggingFace Settings
178
+ </a>
179
+ </div>
180
+
181
+ {/* Error banner */}
182
+ {error && (
183
+ <div
184
+ className="p-4 border border-destructive/30 bg-destructive/5 rounded-xl text-sm text-destructive flex items-start gap-2 mt-4 animate-in fade-in slide-in-from-top-2 duration-200"
185
+ role="alert"
186
+ aria-live="polite"
187
+ >
188
+ <AlertCircle className="w-4 h-4 mt-0.5 shrink-0" />
189
+ <span>{error}</span>
190
+ </div>
191
+ )}
192
+
193
+ {/* Success banner */}
194
+ {success && (
195
+ <div
196
+ className="p-4 border border-primary/20 bg-primary/5 rounded-xl text-sm text-primary flex items-start gap-2 mt-4 animate-in fade-in slide-in-from-top-2 duration-200"
197
+ aria-live="polite"
198
+ >
199
+ <CheckCircle2 className="w-4 h-4 mt-0.5 shrink-0" />
200
+ <span>Token saved successfully</span>
201
+ </div>
202
+ )}
203
+ </form>
204
+
205
+ {/* Footer */}
206
+ <DialogFooter className="mt-4">
207
+ <Button variant="outline" onClick={() => setOpen(false)}>
208
+ Cancel
209
+ </Button>
210
+ <Button
211
+ onClick={handleSave}
212
+ disabled={isSaveDisabled}
213
+ aria-busy={saving}
214
+ title={hasExistingToken ? "Replace existing token with a new one" : undefined}
215
+ >
216
+ {saving ? (
217
+ <>
218
+ <Loader2 className="w-4 h-4 animate-spin mr-1.5" />
219
+ Saving...
220
+ </>
221
+ ) : hasExistingToken ? (
222
+ "Update Token"
223
+ ) : (
224
+ "Save Token"
225
+ )}
226
+ </Button>
227
+ </DialogFooter>
228
+ </DialogContent>
229
+ </Dialog>
230
+ );
231
+ }
frontend/src/components/layout/ContributorsPanel.tsx CHANGED
@@ -1,6 +1,7 @@
1
  "use client";
2
 
3
  import { useState, useEffect } from "react";
 
4
  import { GitBranch, Star, GitPullRequest, Users, X, Trophy, ExternalLink } from "lucide-react";
5
  import { Button } from "@/components/ui/button";
6
  import { api } from "@/lib/api";
@@ -124,7 +125,7 @@ export default function ContributorsPanel({ onClose }: { onClose: () => void })
124
  {medals[i]}
125
  </span>
126
  )}
127
- <img
128
  src={c.avatar_url}
129
  alt={c.login}
130
  width={56}
 
1
  "use client";
2
 
3
  import { useState, useEffect } from "react";
4
+ import Image from "next/image";
5
  import { GitBranch, Star, GitPullRequest, Users, X, Trophy, ExternalLink } from "lucide-react";
6
  import { Button } from "@/components/ui/button";
7
  import { api } from "@/lib/api";
 
125
  {medals[i]}
126
  </span>
127
  )}
128
+ <Image
129
  src={c.avatar_url}
130
  alt={c.login}
131
  width={56}
frontend/src/components/layout/Header.tsx CHANGED
@@ -27,6 +27,7 @@ import {
27
  X,
28
  } from "lucide-react";
29
  import { useTheme } from "next-themes";
 
30
  import { useSyncExternalStore } from "react";
31
 
32
  interface HeaderProps {
 
27
  X,
28
  } from "lucide-react";
29
  import { useTheme } from "next-themes";
30
+
31
  import { useSyncExternalStore } from "react";
32
 
33
  interface HeaderProps {
package-lock.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {
2
+ "name": "PDF-Assistant-RAG",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {}
6
+ }