Add error handling and message improvements
Browse files- src/App.tsx +21 -3
- src/components/ModelInfo.tsx +12 -4
- src/components/ModelLoader.tsx +26 -16
- src/components/Sidebar.tsx +9 -2
- src/components/ui/button.tsx +21 -21
- src/contexts/ModelContext.tsx +6 -1
src/App.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { getModelsByPipeline } from './lib/huggingface'
|
|
| 8 |
import TextGeneration from './components/pipelines/TextGeneration'
|
| 9 |
import FeatureExtraction from './components/pipelines/FeatureExtraction'
|
| 10 |
import ImageClassification from './components/pipelines/ImageClassification'
|
|
|
|
| 11 |
import Sidebar from './components/Sidebar'
|
| 12 |
import ModelReadme from './components/ModelReadme'
|
| 13 |
import { PipelineLayout } from './components/PipelineLayout'
|
|
@@ -18,13 +19,22 @@ function App() {
|
|
| 18 |
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
|
| 19 |
const [isModalOpen, setIsModalOpen] = useState(false)
|
| 20 |
const [isCodeModalOpen, setIsCodeModalOpen] = useState(false)
|
| 21 |
-
const {
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
useEffect(() => {
|
| 25 |
setModelInfo(null)
|
| 26 |
setModels([])
|
| 27 |
setIsFetching(true)
|
|
|
|
|
|
|
| 28 |
|
| 29 |
const fetchModels = async () => {
|
| 30 |
try {
|
|
@@ -36,7 +46,14 @@ function App() {
|
|
| 36 |
}
|
| 37 |
}
|
| 38 |
fetchModels()
|
| 39 |
-
}, [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
return (
|
| 42 |
<div className="relative min-h-screen flex flex-col bg-gradient-to-br from-blue-50 to-indigo-100">
|
|
@@ -67,6 +84,7 @@ function App() {
|
|
| 67 |
{pipeline === 'text-generation' && <TextGeneration />}
|
| 68 |
{pipeline === 'feature-extraction' && <FeatureExtraction />}
|
| 69 |
{pipeline === 'image-classification' && <ImageClassification />}
|
|
|
|
| 70 |
</div>
|
| 71 |
</div>
|
| 72 |
</main>
|
|
|
|
| 8 |
import TextGeneration from './components/pipelines/TextGeneration'
|
| 9 |
import FeatureExtraction from './components/pipelines/FeatureExtraction'
|
| 10 |
import ImageClassification from './components/pipelines/ImageClassification'
|
| 11 |
+
import TextToSpeech from './components/pipelines/TextToSpeech'
|
| 12 |
import Sidebar from './components/Sidebar'
|
| 13 |
import ModelReadme from './components/ModelReadme'
|
| 14 |
import { PipelineLayout } from './components/PipelineLayout'
|
|
|
|
| 19 |
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
|
| 20 |
const [isModalOpen, setIsModalOpen] = useState(false)
|
| 21 |
const [isCodeModalOpen, setIsCodeModalOpen] = useState(false)
|
| 22 |
+
const {
|
| 23 |
+
pipeline,
|
| 24 |
+
setModels,
|
| 25 |
+
setModelInfo,
|
| 26 |
+
modelInfo,
|
| 27 |
+
setIsFetching,
|
| 28 |
+
setErrorText,
|
| 29 |
+
setStatus
|
| 30 |
+
} = useModel()
|
| 31 |
|
| 32 |
useEffect(() => {
|
| 33 |
setModelInfo(null)
|
| 34 |
setModels([])
|
| 35 |
setIsFetching(true)
|
| 36 |
+
setStatus('initiate')
|
| 37 |
+
setErrorText('')
|
| 38 |
|
| 39 |
const fetchModels = async () => {
|
| 40 |
try {
|
|
|
|
| 46 |
}
|
| 47 |
}
|
| 48 |
fetchModels()
|
| 49 |
+
}, [
|
| 50 |
+
setModels,
|
| 51 |
+
setModelInfo,
|
| 52 |
+
setIsFetching,
|
| 53 |
+
setStatus,
|
| 54 |
+
setErrorText,
|
| 55 |
+
pipeline
|
| 56 |
+
])
|
| 57 |
|
| 58 |
return (
|
| 59 |
<div className="relative min-h-screen flex flex-col bg-gradient-to-br from-blue-50 to-indigo-100">
|
|
|
|
| 84 |
{pipeline === 'text-generation' && <TextGeneration />}
|
| 85 |
{pipeline === 'feature-extraction' && <FeatureExtraction />}
|
| 86 |
{pipeline === 'image-classification' && <ImageClassification />}
|
| 87 |
+
{pipeline === 'text-to-speech' && <TextToSpeech />}
|
| 88 |
</div>
|
| 89 |
</div>
|
| 90 |
</main>
|
src/components/ModelInfo.tsx
CHANGED
|
@@ -25,7 +25,14 @@ const ModelInfo = () => {
|
|
| 25 |
return num.toString()
|
| 26 |
}
|
| 27 |
|
| 28 |
-
const {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
const ModelInfoSkeleton = () => (
|
| 31 |
<div className="bg-gradient-to-r from-secondary to-accent px-3 py-3 rounded-lg border border-border space-y-3 animate-pulse w-4/5">
|
|
@@ -73,7 +80,7 @@ const ModelInfo = () => {
|
|
| 73 |
{/* Compatibility Status */}
|
| 74 |
{typeof modelInfo.isCompatible === 'boolean' && (
|
| 75 |
<div className="shrink-0 ">
|
| 76 |
-
{modelInfo.isCompatible ? (
|
| 77 |
<CheckCircle className="w-4 h-4 text-green-500" />
|
| 78 |
) : (
|
| 79 |
<XCircle className="w-4 h-4 text-destructive" />
|
|
@@ -156,10 +163,11 @@ const ModelInfo = () => {
|
|
| 156 |
<ModelLoader />
|
| 157 |
|
| 158 |
{/* Incompatibility Message */}
|
| 159 |
-
{modelInfo.isCompatible === false && modelInfo.incompatibilityReason
|
|
|
|
| 160 |
<div className="bg-destructive/10 border border-destructive/20 rounded-md px-2 py-2">
|
| 161 |
<p className="text-xs text-destructive whitespace-break-spaces">
|
| 162 |
-
{modelInfo.incompatibilityReason}
|
| 163 |
</p>
|
| 164 |
</div>
|
| 165 |
)}
|
|
|
|
| 25 |
return num.toString()
|
| 26 |
}
|
| 27 |
|
| 28 |
+
const {
|
| 29 |
+
models,
|
| 30 |
+
status,
|
| 31 |
+
modelInfo,
|
| 32 |
+
selectedQuantization,
|
| 33 |
+
isFetching,
|
| 34 |
+
errorText
|
| 35 |
+
} = useModel()
|
| 36 |
|
| 37 |
const ModelInfoSkeleton = () => (
|
| 38 |
<div className="bg-gradient-to-r from-secondary to-accent px-3 py-3 rounded-lg border border-border space-y-3 animate-pulse w-4/5">
|
|
|
|
| 80 |
{/* Compatibility Status */}
|
| 81 |
{typeof modelInfo.isCompatible === 'boolean' && (
|
| 82 |
<div className="shrink-0 ">
|
| 83 |
+
{modelInfo.isCompatible && status !== 'error' ? (
|
| 84 |
<CheckCircle className="w-4 h-4 text-green-500" />
|
| 85 |
) : (
|
| 86 |
<XCircle className="w-4 h-4 text-destructive" />
|
|
|
|
| 163 |
<ModelLoader />
|
| 164 |
|
| 165 |
{/* Incompatibility Message */}
|
| 166 |
+
{((modelInfo.isCompatible === false && modelInfo.incompatibilityReason) ||
|
| 167 |
+
errorText) && (
|
| 168 |
<div className="bg-destructive/10 border border-destructive/20 rounded-md px-2 py-2">
|
| 169 |
<p className="text-xs text-destructive whitespace-break-spaces">
|
| 170 |
+
{errorText ? errorText : modelInfo.incompatibilityReason}
|
| 171 |
</p>
|
| 172 |
</div>
|
| 173 |
)}
|
src/components/ModelLoader.tsx
CHANGED
|
@@ -2,12 +2,13 @@ import { useEffect, useCallback, useState } from 'react'
|
|
| 2 |
import { ChevronDown, Loader2, X } from 'lucide-react'
|
| 3 |
import { QuantizationType, WorkerMessage } from '../types'
|
| 4 |
import { useModel } from '../contexts/ModelContext'
|
| 5 |
-
import { getWorker } from '../lib/workerManager'
|
| 6 |
import { Alert, AlertDescription } from './ui/alert'
|
| 7 |
|
| 8 |
const ModelLoader = () => {
|
| 9 |
const [showAlert, setShowAlert] = useState(false)
|
| 10 |
const [alertMessage, setAlertMessage] = useState<React.ReactNode>('')
|
|
|
|
| 11 |
const {
|
| 12 |
modelInfo,
|
| 13 |
selectedQuantization,
|
|
@@ -20,7 +21,8 @@ const ModelLoader = () => {
|
|
| 20 |
setActiveWorker,
|
| 21 |
pipeline,
|
| 22 |
hasBeenLoaded,
|
| 23 |
-
setHasBeenLoaded
|
|
|
|
| 24 |
} = useModel()
|
| 25 |
|
| 26 |
useEffect(() => {
|
|
@@ -57,8 +59,10 @@ const ModelLoader = () => {
|
|
| 57 |
}
|
| 58 |
|
| 59 |
if (!hasBeenLoaded) {
|
|
|
|
| 60 |
setStatus('initiate')
|
| 61 |
setActiveWorker(newWorker)
|
|
|
|
| 62 |
}
|
| 63 |
|
| 64 |
const onMessageReceived = (e: MessageEvent<WorkerMessage>) => {
|
|
@@ -75,24 +79,25 @@ const ModelLoader = () => {
|
|
| 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')
|
| 88 |
const error = e.data.output
|
| 89 |
console.error(error)
|
| 90 |
-
|
|
|
|
| 91 |
setShowAlert(true)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
setTimeout(() => {
|
| 93 |
setShowAlert(false)
|
| 94 |
setAlertMessage('')
|
| 95 |
-
},
|
| 96 |
}
|
| 97 |
}
|
| 98 |
|
|
@@ -100,7 +105,7 @@ const ModelLoader = () => {
|
|
| 100 |
|
| 101 |
return () => {
|
| 102 |
newWorker.removeEventListener('message', onMessageReceived)
|
| 103 |
-
|
| 104 |
}
|
| 105 |
}, [
|
| 106 |
pipeline,
|
|
@@ -110,7 +115,8 @@ const ModelLoader = () => {
|
|
| 110 |
setStatus,
|
| 111 |
setProgress,
|
| 112 |
hasBeenLoaded,
|
| 113 |
-
setHasBeenLoaded
|
|
|
|
| 114 |
])
|
| 115 |
|
| 116 |
useEffect(() => {
|
|
@@ -174,8 +180,10 @@ const ModelLoader = () => {
|
|
| 174 |
{selectedQuantization && (
|
| 175 |
<div className="flex justify-center">
|
| 176 |
<button
|
| 177 |
-
className=
|
| 178 |
-
disabled={
|
|
|
|
|
|
|
| 179 |
onClick={loadModel}
|
| 180 |
>
|
| 181 |
{status === 'loading' && !hasBeenLoaded ? (
|
|
@@ -183,8 +191,10 @@ const ModelLoader = () => {
|
|
| 183 |
<Loader2 className="animate-spin h-4 w-4" />
|
| 184 |
<span>{progress.toFixed(0)}%</span>
|
| 185 |
</>
|
| 186 |
-
) : (
|
| 187 |
<span>{!hasBeenLoaded ? 'Load Model' : 'Model Ready'}</span>
|
|
|
|
|
|
|
| 188 |
)}
|
| 189 |
</button>
|
| 190 |
</div>
|
|
|
|
| 2 |
import { ChevronDown, Loader2, X } from 'lucide-react'
|
| 3 |
import { QuantizationType, WorkerMessage } from '../types'
|
| 4 |
import { useModel } from '../contexts/ModelContext'
|
| 5 |
+
import { getWorker, terminateWorker } from '../lib/workerManager'
|
| 6 |
import { Alert, AlertDescription } from './ui/alert'
|
| 7 |
|
| 8 |
const ModelLoader = () => {
|
| 9 |
const [showAlert, setShowAlert] = useState(false)
|
| 10 |
const [alertMessage, setAlertMessage] = useState<React.ReactNode>('')
|
| 11 |
+
const [lastModel, setLastModel] = useState<string | null>(null)
|
| 12 |
const {
|
| 13 |
modelInfo,
|
| 14 |
selectedQuantization,
|
|
|
|
| 21 |
setActiveWorker,
|
| 22 |
pipeline,
|
| 23 |
hasBeenLoaded,
|
| 24 |
+
setHasBeenLoaded,
|
| 25 |
+
setErrorText
|
| 26 |
} = useModel()
|
| 27 |
|
| 28 |
useEffect(() => {
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
if (!hasBeenLoaded) {
|
| 62 |
+
setErrorText('')
|
| 63 |
setStatus('initiate')
|
| 64 |
setActiveWorker(newWorker)
|
| 65 |
+
setProgress(0)
|
| 66 |
}
|
| 67 |
|
| 68 |
const onMessageReceived = (e: MessageEvent<WorkerMessage>) => {
|
|
|
|
| 79 |
output.file.startsWith('onnx')
|
| 80 |
) {
|
| 81 |
setProgress(output.progress)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
}
|
| 83 |
} else if (status === 'error') {
|
| 84 |
setStatus('error')
|
| 85 |
const error = e.data.output
|
| 86 |
console.error(error)
|
| 87 |
+
const errText = error.split(' WASM error: ')[1]
|
| 88 |
+
setErrorText(errText)
|
| 89 |
setShowAlert(true)
|
| 90 |
+
let time = 3000
|
| 91 |
+
if (!hasBeenLoaded)
|
| 92 |
+
setAlertMessage(error.split('.')[0] + '. See console for details.')
|
| 93 |
+
else {
|
| 94 |
+
setAlertMessage(`${errText}. Refresh the page and try again.`)
|
| 95 |
+
time = 5000
|
| 96 |
+
}
|
| 97 |
setTimeout(() => {
|
| 98 |
setShowAlert(false)
|
| 99 |
setAlertMessage('')
|
| 100 |
+
}, time)
|
| 101 |
}
|
| 102 |
}
|
| 103 |
|
|
|
|
| 105 |
|
| 106 |
return () => {
|
| 107 |
newWorker.removeEventListener('message', onMessageReceived)
|
| 108 |
+
terminateWorker(pipeline)
|
| 109 |
}
|
| 110 |
}, [
|
| 111 |
pipeline,
|
|
|
|
| 115 |
setStatus,
|
| 116 |
setProgress,
|
| 117 |
hasBeenLoaded,
|
| 118 |
+
setHasBeenLoaded,
|
| 119 |
+
setErrorText
|
| 120 |
])
|
| 121 |
|
| 122 |
useEffect(() => {
|
|
|
|
| 180 |
{selectedQuantization && (
|
| 181 |
<div className="flex justify-center">
|
| 182 |
<button
|
| 183 |
+
className={`w-32 py-2 px-4 ${status !== 'error' ? 'bg-green-500 hover:bg-green-600 cursor-pointer' : 'bg-red-500 hover:bg-red-600'} rounded-sm text-white font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm inline-flex items-center text-center justify-center space-x-2`}
|
| 184 |
+
disabled={
|
| 185 |
+
hasBeenLoaded || status === 'loading' || status === 'error'
|
| 186 |
+
}
|
| 187 |
onClick={loadModel}
|
| 188 |
>
|
| 189 |
{status === 'loading' && !hasBeenLoaded ? (
|
|
|
|
| 191 |
<Loader2 className="animate-spin h-4 w-4" />
|
| 192 |
<span>{progress.toFixed(0)}%</span>
|
| 193 |
</>
|
| 194 |
+
) : status !== 'error' ? (
|
| 195 |
<span>{!hasBeenLoaded ? 'Load Model' : 'Model Ready'}</span>
|
| 196 |
+
) : (
|
| 197 |
+
<span>Error</span>
|
| 198 |
)}
|
| 199 |
</button>
|
| 200 |
</div>
|
src/components/Sidebar.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import FeatureExtractionConfig from './pipelines/FeatureExtractionConfig'
|
|
| 8 |
import ZeroShotClassificationConfig from './pipelines/ZeroShotClassificationConfig'
|
| 9 |
import ImageClassificationConfig from './pipelines/ImageClassificationConfig'
|
| 10 |
import TextClassificationConfig from './pipelines/TextClassificationConfig'
|
|
|
|
| 11 |
import { Button } from '@/components/ui/button'
|
| 12 |
import Tooltip from './Tooltip'
|
| 13 |
|
|
@@ -24,7 +25,7 @@ const Sidebar = ({
|
|
| 24 |
setIsModalOpen,
|
| 25 |
setIsCodeModalOpen
|
| 26 |
}: SidebarProps) => {
|
| 27 |
-
const { pipeline, setPipeline } = useModel()
|
| 28 |
|
| 29 |
return (
|
| 30 |
<>
|
|
@@ -95,13 +96,18 @@ const Sidebar = ({
|
|
| 95 |
<ModelInfo />
|
| 96 |
{/* Model README Button */}
|
| 97 |
<div className="flex flex-row mt-2 space-x-4 ">
|
| 98 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
<FileText className="w-4 h-4 flex-shrink-0" />
|
| 100 |
<span>View README.md</span>
|
| 101 |
</Button>
|
| 102 |
<Button
|
| 103 |
variant="outline"
|
| 104 |
onClick={() => setIsCodeModalOpen(true)}
|
|
|
|
| 105 |
>
|
| 106 |
<Code2 className="w-4 h-4 flex-shrink-0" />
|
| 107 |
<span>See Code</span>
|
|
@@ -119,6 +125,7 @@ const Sidebar = ({
|
|
| 119 |
<ImageClassificationConfig />
|
| 120 |
)}
|
| 121 |
{pipeline === 'text-classification' && <TextClassificationConfig />}
|
|
|
|
| 122 |
</div>
|
| 123 |
</div>
|
| 124 |
</div>
|
|
|
|
| 8 |
import ZeroShotClassificationConfig from './pipelines/ZeroShotClassificationConfig'
|
| 9 |
import ImageClassificationConfig from './pipelines/ImageClassificationConfig'
|
| 10 |
import TextClassificationConfig from './pipelines/TextClassificationConfig'
|
| 11 |
+
import TextToSpeechConfig from './pipelines/TextToSpeechConfig'
|
| 12 |
import { Button } from '@/components/ui/button'
|
| 13 |
import Tooltip from './Tooltip'
|
| 14 |
|
|
|
|
| 25 |
setIsModalOpen,
|
| 26 |
setIsCodeModalOpen
|
| 27 |
}: SidebarProps) => {
|
| 28 |
+
const { pipeline, setPipeline, modelInfo } = useModel()
|
| 29 |
|
| 30 |
return (
|
| 31 |
<>
|
|
|
|
| 96 |
<ModelInfo />
|
| 97 |
{/* Model README Button */}
|
| 98 |
<div className="flex flex-row mt-2 space-x-4 ">
|
| 99 |
+
<Button
|
| 100 |
+
variant="outline"
|
| 101 |
+
onClick={() => setIsModalOpen(true)}
|
| 102 |
+
disabled={!modelInfo}
|
| 103 |
+
>
|
| 104 |
<FileText className="w-4 h-4 flex-shrink-0" />
|
| 105 |
<span>View README.md</span>
|
| 106 |
</Button>
|
| 107 |
<Button
|
| 108 |
variant="outline"
|
| 109 |
onClick={() => setIsCodeModalOpen(true)}
|
| 110 |
+
disabled={!modelInfo}
|
| 111 |
>
|
| 112 |
<Code2 className="w-4 h-4 flex-shrink-0" />
|
| 113 |
<span>See Code</span>
|
|
|
|
| 125 |
<ImageClassificationConfig />
|
| 126 |
)}
|
| 127 |
{pipeline === 'text-classification' && <TextClassificationConfig />}
|
| 128 |
+
{pipeline === 'text-to-speech' && <TextToSpeechConfig />}
|
| 129 |
</div>
|
| 130 |
</div>
|
| 131 |
</div>
|
src/components/ui/button.tsx
CHANGED
|
@@ -1,37 +1,37 @@
|
|
| 1 |
-
import * as React from
|
| 2 |
-
import { Slot } from
|
| 3 |
-
import { cva, type VariantProps } from
|
| 4 |
|
| 5 |
-
import { cn } from
|
| 6 |
|
| 7 |
const buttonVariants = cva(
|
| 8 |
-
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
| 9 |
{
|
| 10 |
variants: {
|
| 11 |
variant: {
|
| 12 |
default:
|
| 13 |
-
|
| 14 |
destructive:
|
| 15 |
-
|
| 16 |
outline:
|
| 17 |
-
|
| 18 |
secondary:
|
| 19 |
-
|
| 20 |
ghost:
|
| 21 |
-
|
| 22 |
-
link:
|
| 23 |
},
|
| 24 |
size: {
|
| 25 |
-
default:
|
| 26 |
-
sm:
|
| 27 |
-
lg:
|
| 28 |
-
icon:
|
| 29 |
-
}
|
| 30 |
},
|
| 31 |
defaultVariants: {
|
| 32 |
-
variant:
|
| 33 |
-
size:
|
| 34 |
-
}
|
| 35 |
}
|
| 36 |
)
|
| 37 |
|
|
@@ -41,11 +41,11 @@ function Button({
|
|
| 41 |
size,
|
| 42 |
asChild = false,
|
| 43 |
...props
|
| 44 |
-
}: React.ComponentProps<
|
| 45 |
VariantProps<typeof buttonVariants> & {
|
| 46 |
asChild?: boolean
|
| 47 |
}) {
|
| 48 |
-
const Comp = asChild ? Slot :
|
| 49 |
|
| 50 |
return (
|
| 51 |
<Comp
|
|
|
|
| 1 |
+
import * as React from 'react'
|
| 2 |
+
import { Slot } from '@radix-ui/react-slot'
|
| 3 |
+
import { cva, type VariantProps } from 'class-variance-authority'
|
| 4 |
|
| 5 |
+
import { cn } from '@/lib/utils'
|
| 6 |
|
| 7 |
const buttonVariants = cva(
|
| 8 |
+
"cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
| 9 |
{
|
| 10 |
variants: {
|
| 11 |
variant: {
|
| 12 |
default:
|
| 13 |
+
'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
| 14 |
destructive:
|
| 15 |
+
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
| 16 |
outline:
|
| 17 |
+
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
| 18 |
secondary:
|
| 19 |
+
'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
| 20 |
ghost:
|
| 21 |
+
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
| 22 |
+
link: 'text-primary underline-offset-4 hover:underline'
|
| 23 |
},
|
| 24 |
size: {
|
| 25 |
+
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
| 26 |
+
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
| 27 |
+
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
| 28 |
+
icon: 'size-9'
|
| 29 |
+
}
|
| 30 |
},
|
| 31 |
defaultVariants: {
|
| 32 |
+
variant: 'default',
|
| 33 |
+
size: 'default'
|
| 34 |
+
}
|
| 35 |
}
|
| 36 |
)
|
| 37 |
|
|
|
|
| 41 |
size,
|
| 42 |
asChild = false,
|
| 43 |
...props
|
| 44 |
+
}: React.ComponentProps<'button'> &
|
| 45 |
VariantProps<typeof buttonVariants> & {
|
| 46 |
asChild?: boolean
|
| 47 |
}) {
|
| 48 |
+
const Comp = asChild ? Slot : 'button'
|
| 49 |
|
| 50 |
return (
|
| 51 |
<Comp
|
src/contexts/ModelContext.tsx
CHANGED
|
@@ -25,6 +25,8 @@ interface ModelContextType {
|
|
| 25 |
setIsFetching: (isFetching: boolean) => void
|
| 26 |
hasBeenLoaded: boolean
|
| 27 |
setHasBeenLoaded: (hasBeenLoaded: boolean) => void
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
const ModelContext = createContext<ModelContextType | undefined>(undefined)
|
|
@@ -42,6 +44,7 @@ export function ModelProvider({ children }: { children: React.ReactNode }) {
|
|
| 42 |
const [activeWorker, setActiveWorker] = useState<Worker | null>(null)
|
| 43 |
const [isFetching, setIsFetching] = useState(false)
|
| 44 |
const [hasBeenLoaded, setHasBeenLoaded] = useState(false)
|
|
|
|
| 45 |
|
| 46 |
// set progress to 0 when model is changed
|
| 47 |
useEffect(() => {
|
|
@@ -68,7 +71,9 @@ export function ModelProvider({ children }: { children: React.ReactNode }) {
|
|
| 68 |
isFetching,
|
| 69 |
setIsFetching,
|
| 70 |
hasBeenLoaded,
|
| 71 |
-
setHasBeenLoaded
|
|
|
|
|
|
|
| 72 |
}}
|
| 73 |
>
|
| 74 |
{children}
|
|
|
|
| 25 |
setIsFetching: (isFetching: boolean) => void
|
| 26 |
hasBeenLoaded: boolean
|
| 27 |
setHasBeenLoaded: (hasBeenLoaded: boolean) => void
|
| 28 |
+
errorText: string
|
| 29 |
+
setErrorText: (errorText: string) => void
|
| 30 |
}
|
| 31 |
|
| 32 |
const ModelContext = createContext<ModelContextType | undefined>(undefined)
|
|
|
|
| 44 |
const [activeWorker, setActiveWorker] = useState<Worker | null>(null)
|
| 45 |
const [isFetching, setIsFetching] = useState(false)
|
| 46 |
const [hasBeenLoaded, setHasBeenLoaded] = useState(false)
|
| 47 |
+
const [errorText, setErrorText] = useState('')
|
| 48 |
|
| 49 |
// set progress to 0 when model is changed
|
| 50 |
useEffect(() => {
|
|
|
|
| 71 |
isFetching,
|
| 72 |
setIsFetching,
|
| 73 |
hasBeenLoaded,
|
| 74 |
+
setHasBeenLoaded,
|
| 75 |
+
errorText,
|
| 76 |
+
setErrorText
|
| 77 |
}}
|
| 78 |
>
|
| 79 |
{children}
|