Improve modal animations and UI interactions
Browse files- src/App.tsx +1 -1
- src/components/MarkdownRenderer.tsx +1 -1
- src/components/Modal.tsx +33 -13
- src/components/ModelCode.tsx +28 -7
- src/components/ModelLoader.tsx +7 -7
- src/components/ModelReadme.tsx +1 -1
- src/components/ModelSelector.tsx +6 -2
- src/components/PipelineSelector.tsx +2 -2
- src/lib/huggingface.ts +3 -3
src/App.tsx
CHANGED
@@ -88,7 +88,7 @@ function App() {
|
|
88 |
{modelInfo?.readme && (
|
89 |
<ModelReadme
|
90 |
readme={modelInfo.readme}
|
91 |
-
modelName={modelInfo.name}
|
92 |
isModalOpen={isModalOpen}
|
93 |
setIsModalOpen={setIsModalOpen}
|
94 |
/>
|
|
|
88 |
{modelInfo?.readme && (
|
89 |
<ModelReadme
|
90 |
readme={modelInfo.readme}
|
91 |
+
modelName={modelInfo.baseId ? modelInfo.baseId : modelInfo.name}
|
92 |
isModalOpen={isModalOpen}
|
93 |
setIsModalOpen={setIsModalOpen}
|
94 |
/>
|
src/components/MarkdownRenderer.tsx
CHANGED
@@ -26,7 +26,7 @@ const MarkdownRenderer = ({ content }: MarkdownRendererProps) => {
|
|
26 |
style={oneLight}
|
27 |
language={match[1]}
|
28 |
PreTag="div"
|
29 |
-
className="rounded-md my-4 border
|
30 |
{...props}
|
31 |
>
|
32 |
{String(children).replace(/\n$/, '')}
|
|
|
26 |
style={oneLight}
|
27 |
language={match[1]}
|
28 |
PreTag="div"
|
29 |
+
className="rounded-md my-4 border text-sm"
|
30 |
{...props}
|
31 |
>
|
32 |
{String(children).replace(/\n$/, '')}
|
src/components/Modal.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import React, { useEffect } from 'react'
|
2 |
import { X } from 'lucide-react'
|
3 |
|
4 |
interface ModalProps {
|
@@ -26,6 +26,11 @@ const Modal: React.FC<ModalProps> = ({
|
|
26 |
children,
|
27 |
maxWidth = '4xl'
|
28 |
}) => {
|
|
|
|
|
|
|
|
|
|
|
29 |
useEffect(() => {
|
30 |
const handleEscape = (e: KeyboardEvent) => {
|
31 |
if (e.key === 'Escape') {
|
@@ -34,17 +39,24 @@ const Modal: React.FC<ModalProps> = ({
|
|
34 |
}
|
35 |
|
36 |
if (isOpen) {
|
37 |
-
|
38 |
document.body.style.overflow = 'hidden'
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
}
|
45 |
}, [isOpen, onClose])
|
46 |
|
47 |
-
|
|
|
48 |
|
49 |
const maxWidthClasses = {
|
50 |
sm: 'max-w-sm',
|
@@ -63,22 +75,30 @@ const Modal: React.FC<ModalProps> = ({
|
|
63 |
<div className="fixed inset-0 z-50 overflow-y-auto">
|
64 |
{/* Backdrop */}
|
65 |
<div
|
66 |
-
className=
|
|
|
|
|
67 |
onClick={onClose}
|
68 |
/>
|
69 |
|
70 |
{/* Modal */}
|
71 |
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
72 |
<div
|
73 |
-
className={`relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full ${
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
onClick={(e) => e.stopPropagation()}
|
75 |
>
|
76 |
{/* Header */}
|
77 |
-
<div className="flex items-center justify-between
|
78 |
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
79 |
<button
|
80 |
onClick={onClose}
|
81 |
-
className="rounded-md p-2 text-gray-400 hover:
|
82 |
>
|
83 |
<span className="sr-only">Close</span>
|
84 |
<X className="h-5 w-5" />
|
@@ -86,7 +106,7 @@ const Modal: React.FC<ModalProps> = ({
|
|
86 |
</div>
|
87 |
|
88 |
{/* Content */}
|
89 |
-
<div className="
|
90 |
{children}
|
91 |
</div>
|
92 |
</div>
|
|
|
1 |
+
import React, { useState, useEffect } from 'react'
|
2 |
import { X } from 'lucide-react'
|
3 |
|
4 |
interface ModalProps {
|
|
|
26 |
children,
|
27 |
maxWidth = '4xl'
|
28 |
}) => {
|
29 |
+
// State to control if the modal is in the DOM
|
30 |
+
const [isRendered, setIsRendered] = useState(isOpen)
|
31 |
+
// State to control the animation classes
|
32 |
+
const [isAnimating, setIsAnimating] = useState(false)
|
33 |
+
|
34 |
useEffect(() => {
|
35 |
const handleEscape = (e: KeyboardEvent) => {
|
36 |
if (e.key === 'Escape') {
|
|
|
39 |
}
|
40 |
|
41 |
if (isOpen) {
|
42 |
+
setIsRendered(true)
|
43 |
document.body.style.overflow = 'hidden'
|
44 |
+
document.addEventListener('keydown', handleEscape)
|
45 |
+
const animationTimeout = setTimeout(() => setIsAnimating(true), 20)
|
46 |
+
return () => clearTimeout(animationTimeout)
|
47 |
+
} else {
|
48 |
+
setIsAnimating(false)
|
49 |
+
const unmountTimeout = setTimeout(() => {
|
50 |
+
setIsRendered(false)
|
51 |
+
document.body.style.overflow = 'unset'
|
52 |
+
document.removeEventListener('keydown', handleEscape)
|
53 |
+
}, 300)
|
54 |
+
return () => clearTimeout(unmountTimeout)
|
55 |
}
|
56 |
}, [isOpen, onClose])
|
57 |
|
58 |
+
// Unmount the component completely when not rendered
|
59 |
+
if (!isRendered) return null
|
60 |
|
61 |
const maxWidthClasses = {
|
62 |
sm: 'max-w-sm',
|
|
|
75 |
<div className="fixed inset-0 z-50 overflow-y-auto">
|
76 |
{/* Backdrop */}
|
77 |
<div
|
78 |
+
className={`fixed inset-0 bg-black transition-opacity duration-300 ease-in-out ${
|
79 |
+
isAnimating ? 'opacity-50' : 'opacity-0'
|
80 |
+
}`}
|
81 |
onClick={onClose}
|
82 |
/>
|
83 |
|
84 |
{/* Modal */}
|
85 |
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
86 |
<div
|
87 |
+
className={`relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all duration-300 ease-in-out sm:my-8 sm:w-full ${
|
88 |
+
maxWidthClasses[maxWidth]
|
89 |
+
} ${
|
90 |
+
isAnimating
|
91 |
+
? 'opacity-100 translate-y-0 sm:scale-100'
|
92 |
+
: 'opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
93 |
+
}`}
|
94 |
onClick={(e) => e.stopPropagation()}
|
95 |
>
|
96 |
{/* Header */}
|
97 |
+
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
98 |
<h3 className="text-lg font-semibold text-gray-900">{title}</h3>
|
99 |
<button
|
100 |
onClick={onClose}
|
101 |
+
className="rounded-md p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
102 |
>
|
103 |
<span className="sr-only">Close</span>
|
104 |
<X className="h-5 w-5" />
|
|
|
106 |
</div>
|
107 |
|
108 |
{/* Content */}
|
109 |
+
<div className="max-h-[calc(100vh-200px)] overflow-y-auto px-6 py-4">
|
110 |
{children}
|
111 |
</div>
|
112 |
</div>
|
src/components/ModelCode.tsx
CHANGED
@@ -3,7 +3,7 @@ import Modal from './Modal'
|
|
3 |
import MarkdownRenderer from './MarkdownRenderer'
|
4 |
import { useModel } from '@/contexts/ModelContext'
|
5 |
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
6 |
-
import { useState } from 'react'
|
7 |
|
8 |
interface ModelCodeProps {
|
9 |
isCodeModalOpen: boolean
|
@@ -12,7 +12,23 @@ interface ModelCodeProps {
|
|
12 |
|
13 |
const ModelCode = ({ isCodeModalOpen, setIsCodeModalOpen }: ModelCodeProps) => {
|
14 |
const [isCopied, setIsCopied] = useState(false)
|
|
|
|
|
|
|
15 |
const { modelInfo, pipeline, selectedQuantization } = useModel()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
if (!modelInfo) return null
|
17 |
|
18 |
const title = (
|
@@ -24,7 +40,6 @@ const ModelCode = ({ isCodeModalOpen, setIsCodeModalOpen }: ModelCodeProps) => {
|
|
24 |
rel="noopener noreferrer"
|
25 |
>
|
26 |
<ExternalLink className="w-3 h-3 inline-block mr-1" />
|
27 |
-
|
28 |
{modelInfo.name}
|
29 |
</a>
|
30 |
</div>
|
@@ -107,7 +122,6 @@ print(result)
|
|
107 |
setIsCopied(true)
|
108 |
setTimeout(() => setIsCopied(false), 2000)
|
109 |
}
|
110 |
-
|
111 |
const pipelineName = pipeline
|
112 |
.split('-')
|
113 |
.map((word, index) => word.charAt(0).toUpperCase() + word.slice(1))
|
@@ -121,6 +135,7 @@ print(result)
|
|
121 |
title={title}
|
122 |
maxWidth="5xl"
|
123 |
>
|
|
|
124 |
<div className="text-sm max-w-none px-4">
|
125 |
<div className="flex flex-row">
|
126 |
<img src="/javascript-logo.svg" className="w-6 h-6 mr-1 rounded" />
|
@@ -133,7 +148,7 @@ print(result)
|
|
133 |
target="_blank"
|
134 |
rel="noopener noreferrer"
|
135 |
>
|
136 |
-
Read about {pipeline}
|
137 |
</a>
|
138 |
</div>
|
139 |
<div className="relative">
|
@@ -159,7 +174,7 @@ print(result)
|
|
159 |
target="_blank"
|
160 |
rel="noopener noreferrer"
|
161 |
>
|
162 |
-
Read about {pipeline}
|
163 |
</a>
|
164 |
<div className="relative">
|
165 |
<div className="absolute right-0 top-0 mt-2 mr-2">
|
@@ -176,8 +191,14 @@ print(result)
|
|
176 |
</div>
|
177 |
</div>
|
178 |
</div>
|
179 |
-
{
|
180 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
181 |
<Alert>
|
182 |
<CopyCheck className="w-4 h-4 opacity-60" />
|
183 |
<AlertDescription>Copied!</AlertDescription>
|
|
|
3 |
import MarkdownRenderer from './MarkdownRenderer'
|
4 |
import { useModel } from '@/contexts/ModelContext'
|
5 |
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
6 |
+
import { useState, useEffect } from 'react'
|
7 |
|
8 |
interface ModelCodeProps {
|
9 |
isCodeModalOpen: boolean
|
|
|
12 |
|
13 |
const ModelCode = ({ isCodeModalOpen, setIsCodeModalOpen }: ModelCodeProps) => {
|
14 |
const [isCopied, setIsCopied] = useState(false)
|
15 |
+
const [showAlert, setShowAlert] = useState(false)
|
16 |
+
const [animateAlert, setAnimateAlert] = useState(false)
|
17 |
+
|
18 |
const { modelInfo, pipeline, selectedQuantization } = useModel()
|
19 |
+
|
20 |
+
useEffect(() => {
|
21 |
+
if (isCopied) {
|
22 |
+
setShowAlert(true)
|
23 |
+
const enterTimeout = setTimeout(() => setAnimateAlert(true), 20)
|
24 |
+
return () => clearTimeout(enterTimeout)
|
25 |
+
} else {
|
26 |
+
setAnimateAlert(false)
|
27 |
+
const exitTimeout = setTimeout(() => setShowAlert(false), 300) // Match duration-300
|
28 |
+
return () => clearTimeout(exitTimeout)
|
29 |
+
}
|
30 |
+
}, [isCopied])
|
31 |
+
|
32 |
if (!modelInfo) return null
|
33 |
|
34 |
const title = (
|
|
|
40 |
rel="noopener noreferrer"
|
41 |
>
|
42 |
<ExternalLink className="w-3 h-3 inline-block mr-1" />
|
|
|
43 |
{modelInfo.name}
|
44 |
</a>
|
45 |
</div>
|
|
|
122 |
setIsCopied(true)
|
123 |
setTimeout(() => setIsCopied(false), 2000)
|
124 |
}
|
|
|
125 |
const pipelineName = pipeline
|
126 |
.split('-')
|
127 |
.map((word, index) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
|
135 |
title={title}
|
136 |
maxWidth="5xl"
|
137 |
>
|
138 |
+
{/* ... (all your modal content JSX is unchanged) */}
|
139 |
<div className="text-sm max-w-none px-4">
|
140 |
<div className="flex flex-row">
|
141 |
<img src="/javascript-logo.svg" className="w-6 h-6 mr-1 rounded" />
|
|
|
148 |
target="_blank"
|
149 |
rel="noopener noreferrer"
|
150 |
>
|
151 |
+
Read about {pipeline} in Transformers.js documentation
|
152 |
</a>
|
153 |
</div>
|
154 |
<div className="relative">
|
|
|
174 |
target="_blank"
|
175 |
rel="noopener noreferrer"
|
176 |
>
|
177 |
+
Read about {pipeline} in Transformers documentation
|
178 |
</a>
|
179 |
<div className="relative">
|
180 |
<div className="absolute right-0 top-0 mt-2 mr-2">
|
|
|
191 |
</div>
|
192 |
</div>
|
193 |
</div>
|
194 |
+
{showAlert && (
|
195 |
+
<div
|
196 |
+
className={`absolute top-4 left-1/2 -translate-x-1/2 transition-all duration-300 ease-in-out ${
|
197 |
+
animateAlert
|
198 |
+
? 'opacity-100 translate-y-0'
|
199 |
+
: 'opacity-0 -translate-y-4'
|
200 |
+
}`}
|
201 |
+
>
|
202 |
<Alert>
|
203 |
<CopyCheck className="w-4 h-4 opacity-60" />
|
204 |
<AlertDescription>Copied!</AlertDescription>
|
src/components/ModelLoader.tsx
CHANGED
@@ -75,13 +75,13 @@ const ModelLoader = () => {
|
|
75 |
output.file.startsWith('onnx')
|
76 |
) {
|
77 |
setProgress(output.progress)
|
78 |
-
setShowAlert(true)
|
79 |
-
setAlertMessage(
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
)
|
85 |
}
|
86 |
} else if (status === 'error') {
|
87 |
setStatus('error')
|
|
|
75 |
output.file.startsWith('onnx')
|
76 |
) {
|
77 |
setProgress(output.progress)
|
78 |
+
// setShowAlert(true)
|
79 |
+
// setAlertMessage(
|
80 |
+
// <div className="flex items-center">
|
81 |
+
// <Loader2 className="animate-spin h-4 w-4 mr-2" />
|
82 |
+
// Loading Model
|
83 |
+
// </div>
|
84 |
+
// )
|
85 |
}
|
86 |
} else if (status === 'error') {
|
87 |
setStatus('error')
|
src/components/ModelReadme.tsx
CHANGED
@@ -27,7 +27,7 @@ const ModelReadme = ({
|
|
27 |
|
28 |
{modelName}
|
29 |
</a>
|
30 |
-
<span className=" text-gray-
|
31 |
</div>
|
32 |
)
|
33 |
|
|
|
27 |
|
28 |
{modelName}
|
29 |
</a>
|
30 |
+
<span className=" text-gray-300">README.md</span>
|
31 |
</div>
|
32 |
)
|
33 |
|
src/components/ModelSelector.tsx
CHANGED
@@ -114,7 +114,6 @@ function ModelSelector() {
|
|
114 |
),
|
115 |
widgetData: modelInfoResponse.widgetData
|
116 |
}
|
117 |
-
console.log(modelInfo)
|
118 |
setModelInfo(modelInfo)
|
119 |
setIsCustomModel(isCustom)
|
120 |
setIsFetching(false)
|
@@ -127,12 +126,17 @@ function ModelSelector() {
|
|
127 |
[setModelInfo, pipeline, setIsFetching]
|
128 |
)
|
129 |
|
130 |
-
// Reset custom model state when pipeline changes
|
131 |
useEffect(() => {
|
|
|
|
|
132 |
setIsCustomModel(false)
|
133 |
setShowCustomInput(false)
|
134 |
setCustomModelName('')
|
135 |
setCustomModelError('')
|
|
|
|
|
|
|
|
|
136 |
}, [pipeline])
|
137 |
|
138 |
// Update modelInfo to first model when models are loaded and no custom model is selected
|
|
|
114 |
),
|
115 |
widgetData: modelInfoResponse.widgetData
|
116 |
}
|
|
|
117 |
setModelInfo(modelInfo)
|
118 |
setIsCustomModel(isCustom)
|
119 |
setIsFetching(false)
|
|
|
126 |
[setModelInfo, pipeline, setIsFetching]
|
127 |
)
|
128 |
|
|
|
129 |
useEffect(() => {
|
130 |
+
// Reset custom model state when pipeline changes
|
131 |
+
|
132 |
setIsCustomModel(false)
|
133 |
setShowCustomInput(false)
|
134 |
setCustomModelName('')
|
135 |
setCustomModelError('')
|
136 |
+
|
137 |
+
if (pipeline !== 'feature-extraction') {
|
138 |
+
setSortBy('downloads')
|
139 |
+
}
|
140 |
}, [pipeline])
|
141 |
|
142 |
// Update modelInfo to first model when models are loaded and no custom model is selected
|
src/components/PipelineSelector.tsx
CHANGED
@@ -11,8 +11,8 @@ export const supportedPipelines = [
|
|
11 |
'feature-extraction',
|
12 |
'image-classification',
|
13 |
'text-generation',
|
14 |
-
'
|
15 |
-
'
|
16 |
// 'summarization',
|
17 |
// 'translation'
|
18 |
]
|
|
|
11 |
'feature-extraction',
|
12 |
'image-classification',
|
13 |
'text-generation',
|
14 |
+
'text-classification',
|
15 |
+
'zero-shot-classification'
|
16 |
// 'summarization',
|
17 |
// 'translation'
|
18 |
]
|
src/lib/huggingface.ts
CHANGED
@@ -132,7 +132,7 @@ const getModelsByPipeline = async (
|
|
132 |
): Promise<ModelInfoResponse[]> => {
|
133 |
// Second search with search=onnx
|
134 |
const response1 = await fetch(
|
135 |
-
`https://huggingface.co/api/models?filter=${pipelineTag}&search=onnx-community&sort=createdAt&limit=
|
136 |
{
|
137 |
method: 'GET'
|
138 |
}
|
@@ -175,10 +175,10 @@ const getModelsByPipeline = async (
|
|
175 |
!model.id.includes('ms-marco') &&
|
176 |
!model.id.includes('MiniLM')
|
177 |
)
|
178 |
-
.slice(0,
|
179 |
}
|
180 |
|
181 |
-
return uniqueModels.slice(0,
|
182 |
}
|
183 |
|
184 |
const getModelsByPipelineCustom = async (
|
|
|
132 |
): Promise<ModelInfoResponse[]> => {
|
133 |
// Second search with search=onnx
|
134 |
const response1 = await fetch(
|
135 |
+
`https://huggingface.co/api/models?filter=${pipelineTag}&search=onnx-community&sort=createdAt&limit=15`,
|
136 |
{
|
137 |
method: 'GET'
|
138 |
}
|
|
|
175 |
!model.id.includes('ms-marco') &&
|
176 |
!model.id.includes('MiniLM')
|
177 |
)
|
178 |
+
.slice(0, 30)
|
179 |
}
|
180 |
|
181 |
+
return uniqueModels.slice(0, 30)
|
182 |
}
|
183 |
|
184 |
const getModelsByPipelineCustom = async (
|