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 (
|