philschmid HF staff commited on
Commit
6108f36
1 Parent(s): c297c27
components.json CHANGED
@@ -11,7 +11,7 @@
11
  "prefix": ""
12
  },
13
  "aliases": {
14
- "components": "src/components",
15
- "utils": "src/lib/utils"
16
  }
17
  }
 
11
  "prefix": ""
12
  },
13
  "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils"
16
  }
17
  }
package-lock.json CHANGED
@@ -9,7 +9,9 @@
9
  "version": "0.0.0",
10
  "dependencies": {
11
  "@radix-ui/react-checkbox": "^1.1.1",
 
12
  "@radix-ui/react-select": "^2.1.1",
 
13
  "class-variance-authority": "^0.7.0",
14
  "clsx": "^2.1.1",
15
  "lucide-react": "^0.399.0",
@@ -1159,6 +1161,35 @@
1159
  }
1160
  }
1161
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1162
  "node_modules/@radix-ui/react-collection": {
1163
  "version": "1.1.0",
1164
  "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
 
9
  "version": "0.0.0",
10
  "dependencies": {
11
  "@radix-ui/react-checkbox": "^1.1.1",
12
+ "@radix-ui/react-collapsible": "^1.1.0",
13
  "@radix-ui/react-select": "^2.1.1",
14
+ "@radix-ui/react-slot": "^1.1.0",
15
  "class-variance-authority": "^0.7.0",
16
  "clsx": "^2.1.1",
17
  "lucide-react": "^0.399.0",
 
1161
  }
1162
  }
1163
  },
1164
+ "node_modules/@radix-ui/react-collapsible": {
1165
+ "version": "1.1.0",
1166
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz",
1167
+ "integrity": "sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==",
1168
+ "dependencies": {
1169
+ "@radix-ui/primitive": "1.1.0",
1170
+ "@radix-ui/react-compose-refs": "1.1.0",
1171
+ "@radix-ui/react-context": "1.1.0",
1172
+ "@radix-ui/react-id": "1.1.0",
1173
+ "@radix-ui/react-presence": "1.1.0",
1174
+ "@radix-ui/react-primitive": "2.0.0",
1175
+ "@radix-ui/react-use-controllable-state": "1.1.0",
1176
+ "@radix-ui/react-use-layout-effect": "1.1.0"
1177
+ },
1178
+ "peerDependencies": {
1179
+ "@types/react": "*",
1180
+ "@types/react-dom": "*",
1181
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1182
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1183
+ },
1184
+ "peerDependenciesMeta": {
1185
+ "@types/react": {
1186
+ "optional": true
1187
+ },
1188
+ "@types/react-dom": {
1189
+ "optional": true
1190
+ }
1191
+ }
1192
+ },
1193
  "node_modules/@radix-ui/react-collection": {
1194
  "version": "1.1.0",
1195
  "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
package.json CHANGED
@@ -11,7 +11,9 @@
11
  },
12
  "dependencies": {
13
  "@radix-ui/react-checkbox": "^1.1.1",
 
14
  "@radix-ui/react-select": "^2.1.1",
 
15
  "class-variance-authority": "^0.7.0",
16
  "clsx": "^2.1.1",
17
  "lucide-react": "^0.399.0",
 
11
  },
12
  "dependencies": {
13
  "@radix-ui/react-checkbox": "^1.1.1",
14
+ "@radix-ui/react-collapsible": "^1.1.0",
15
  "@radix-ui/react-select": "^2.1.1",
16
+ "@radix-ui/react-slot": "^1.1.0",
17
  "class-variance-authority": "^0.7.0",
18
  "clsx": "^2.1.1",
19
  "lucide-react": "^0.399.0",
src/App.tsx CHANGED
@@ -4,6 +4,9 @@ import { Checkbox } from '@/components/ui/checkbox'
4
  import { Input } from '@/components/ui/input'
5
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
6
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
 
 
 
7
  import { mockData } from './lib/data'
8
 
9
  export interface Model {
@@ -23,12 +26,12 @@ const App: React.FC = () => {
23
  const [comparisonModels, setComparisonModels] = useState<string[]>([])
24
  const [inputTokens, setInputTokens] = useState<number>(1)
25
  const [outputTokens, setOutputTokens] = useState<number>(1)
26
- const [selectedProvider, setSelectedProvider] = useState<string>('All')
27
- const [selectedModel, setSelectedModel] = useState<string>('All')
 
28
 
29
  useEffect(() => {
30
  setData(mockData)
31
- // Set default comparison models
32
  setComparisonModels(['OpenAI:GPT-4o', 'Anthropic:Claude 3.5 (Sonnet)', 'Google Vertex AI:Gemini 1.5 Pro'])
33
  }, [])
34
 
@@ -39,59 +42,48 @@ const App: React.FC = () => {
39
  const calculateComparison = (modelPrice: number, comparisonPrice: number): string => {
40
  return (((modelPrice - comparisonPrice) / comparisonPrice) * 100).toFixed(2)
41
  }
42
-
43
  const filteredData = data.filter((provider) => {
44
- if (selectedProvider !== 'All' && provider.provider !== selectedProvider) {
45
  return false
46
  }
47
- if (selectedModel !== 'All') {
48
- return provider.models.some((model) => model.name === selectedModel)
49
  }
50
  return true
51
  })
52
 
 
 
 
 
53
  return (
54
  <Card className="w-full max-w-6xl mx-auto">
55
  <CardHeader>
56
  <CardTitle>LLM Pricing Comparison Tool</CardTitle>
57
  </CardHeader>
58
  <CardContent>
59
- <div className="">
60
  <h3 className="text-lg font-semibold mb-2">Select Comparison Models</h3>
61
-
62
- <div className="flex gap-4 mb-4">
63
- <div className="min-w-96 ">
64
- <div className="flex-1">
65
- <label htmlFor="inputTokens" className="block text-sm font-medium text-gray-700">
66
- Input Tokens (millions)
67
- </label>
68
- <Input
69
- id="inputTokens"
70
- type="number"
71
- value={inputTokens}
72
- onChange={(e) => setInputTokens(Number(e.target.value))}
73
- className="mt-1"
74
- />
75
- </div>
76
- <div className="flex-1">
77
- <label htmlFor="outputTokens" className="block text-sm font-medium text-gray-700">
78
- Output Tokens (millions)
79
- </label>
80
- <Input
81
- id="outputTokens"
82
- type="number"
83
- value={outputTokens}
84
- onChange={(e) => setOutputTokens(Number(e.target.value))}
85
- className="mt-1"
86
- />
87
- </div>
88
- </div>
89
-
90
- <div>
91
- <div className="flex flex-wrap gap-4">
92
- {data.flatMap((provider) =>
93
- provider.models.map((model) => (
94
- <div key={`${provider.provider}:${model.name}`} className="flex items-center space-x-2">
95
  <Checkbox
96
  id={`${provider.provider}:${model.name}`}
97
  checked={comparisonModels.includes(`${provider.provider}:${model.name}`)}
@@ -109,135 +101,156 @@ const App: React.FC = () => {
109
  htmlFor={`${provider.provider}:${model.name}`}
110
  className="text-sm font-medium text-gray-700"
111
  >
112
- {provider.provider}: {model.name}
113
  </label>
114
  </div>
115
- ))
116
- )}
117
- </div>
118
- </div>
119
- </div>
120
-
121
- <div className="flex gap-4 mb-4">
122
- <Select onValueChange={(value) => setSelectedProvider(value)} defaultValue="All">
123
- <SelectTrigger className="w-[180px]">
124
- <SelectValue placeholder="Select Provider" />
125
- </SelectTrigger>
126
- <SelectContent>
127
- <SelectItem value="All">All Providers</SelectItem>
128
- {data.map((provider) => (
129
- <SelectItem key={provider.provider} value={provider.provider}>
130
- {provider.provider}
131
- </SelectItem>
132
- ))}
133
- </SelectContent>
134
- </Select>
135
-
136
- <Select onValueChange={(value) => setSelectedModel(value)} defaultValue="All">
137
- <SelectTrigger className="w-[180px]">
138
- <SelectValue placeholder="Select Model" />
139
- </SelectTrigger>
140
- <SelectContent>
141
- <SelectItem value="All">All Models</SelectItem>
142
- {data
143
- .flatMap((provider) => provider.models)
144
- .map((model) => (
145
- <SelectItem key={model.name} value={model.name}>
146
- {model.name}
147
- </SelectItem>
148
  ))}
149
- </SelectContent>
150
- </Select>
 
151
  </div>
 
152
 
153
- <p className="italic text-sm text-muted-foreground">
154
- Note: If you use Amazon Bedrock or Azure prices for Anthropic, Cohere or OpenAI should be the same.
155
- </p>
156
- <Table>
157
- <TableHeader>
158
- <TableRow>
159
- <TableHead rowSpan={2}>Provider</TableHead>
160
- <TableHead rowSpan={2}>Model</TableHead>
161
- <TableHead rowSpan={2}>Input Price (per 1M tokens)</TableHead>
162
- <TableHead rowSpan={2}>Output Price (per 1M tokens)</TableHead>
163
- <TableHead rowSpan={2}>Total Price</TableHead>
164
- {comparisonModels.map((model) => (
165
- <TableHead key={model} colSpan={2}>
166
- Compared to {model}
167
- </TableHead>
168
- ))}
169
- </TableRow>
170
- <TableRow>
171
- {comparisonModels.flatMap((model) => [
172
- <TableHead key={`${model}-input`}>Input</TableHead>,
173
- <TableHead key={`${model}-output`}>Output</TableHead>,
174
- ])}
175
- </TableRow>
176
- </TableHeader>
177
- <TableBody>
178
- {filteredData.flatMap((provider) =>
179
- provider.models
180
- .filter((model) => selectedModel === 'All' || model.name === selectedModel)
181
- .map((model) => (
182
- <TableRow key={`${provider.provider}-${model.name}`}>
183
- <TableCell>
184
- <a href={provider.uri} className="underline">
185
- {provider.provider}
186
- </a>
187
- </TableCell>
188
- <TableCell>{model.name}</TableCell>
189
- <TableCell>${model.inputPrice.toFixed(2)}</TableCell>
190
- <TableCell>${model.outputPrice.toFixed(2)}</TableCell>
191
- <TableCell className="font-bold border border-r-1 border-r-slate-600 border-l-0">
192
- $
193
- {(
194
- calculatePrice(model.inputPrice, inputTokens) +
195
- calculatePrice(model.outputPrice, outputTokens)
196
- ).toFixed(2)}
197
- </TableCell>
198
- {comparisonModels.flatMap((comparisonModel) => {
199
- const [comparisonProvider, comparisonModelName] = comparisonModel.split(':')
200
- const comparisonModelData = data
201
- .find((p) => p.provider === comparisonProvider)
202
- ?.models.find((m) => m.name === comparisonModelName)!
203
- return [
204
- <TableCell
205
- key={`${comparisonModel}-input`}
206
- className={`${
207
- parseFloat(calculateComparison(model.inputPrice, comparisonModelData.inputPrice)) < 0
208
- ? 'bg-green-100'
209
- : parseFloat(calculateComparison(model.inputPrice, comparisonModelData.inputPrice)) > 0
210
- ? 'bg-red-100'
211
- : ''
212
- }`}
213
- >
214
- {`${provider.provider}:${model.name}` === comparisonModel
215
- ? '0.00%'
216
- : `${calculateComparison(model.inputPrice, comparisonModelData.inputPrice)}%`}
217
- </TableCell>,
218
- <TableCell
219
- key={`${comparisonModel}-output`}
220
- className={`${
221
- parseFloat(calculateComparison(model.outputPrice, comparisonModelData.outputPrice)) < 0
222
- ? 'bg-green-100'
223
- : parseFloat(calculateComparison(model.outputPrice, comparisonModelData.outputPrice)) >
224
- 0
225
- ? 'bg-red-100'
226
- : ''
227
- }`}
228
- >
229
- {`${provider.provider}:${model.name}` === comparisonModel
230
- ? '0.00%'
231
- : `${calculateComparison(model.outputPrice, comparisonModelData.outputPrice)}%`}
232
- </TableCell>,
233
- ]
234
- })}
235
- </TableRow>
236
- ))
237
- )}
238
- </TableBody>
239
- </Table>
240
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  </CardContent>
242
  </Card>
243
  )
 
4
  import { Input } from '@/components/ui/input'
5
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
6
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
7
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
8
+ import { Button } from '@/components/ui/button'
9
+ import { ChevronDown, ChevronRight } from 'lucide-react'
10
  import { mockData } from './lib/data'
11
 
12
  export interface Model {
 
26
  const [comparisonModels, setComparisonModels] = useState<string[]>([])
27
  const [inputTokens, setInputTokens] = useState<number>(1)
28
  const [outputTokens, setOutputTokens] = useState<number>(1)
29
+ const [selectedProviders, setSelectedProviders] = useState<string[]>([])
30
+ const [selectedModels, setSelectedModels] = useState<string[]>([])
31
+ const [expandedProviders, setExpandedProviders] = useState<string[]>([])
32
 
33
  useEffect(() => {
34
  setData(mockData)
 
35
  setComparisonModels(['OpenAI:GPT-4o', 'Anthropic:Claude 3.5 (Sonnet)', 'Google Vertex AI:Gemini 1.5 Pro'])
36
  }, [])
37
 
 
42
  const calculateComparison = (modelPrice: number, comparisonPrice: number): string => {
43
  return (((modelPrice - comparisonPrice) / comparisonPrice) * 100).toFixed(2)
44
  }
 
45
  const filteredData = data.filter((provider) => {
46
+ if (selectedProviders.length > 0 && !selectedProviders.includes(provider.provider)) {
47
  return false
48
  }
49
+ if (selectedModels.length > 0) {
50
+ return provider.models.some((model) => selectedModels.includes(model.name))
51
  }
52
  return true
53
  })
54
 
55
+ const toggleProviderExpansion = (provider: string) => {
56
+ setExpandedProviders((prev) => (prev.includes(provider) ? prev.filter((p) => p !== provider) : [...prev, provider]))
57
+ }
58
+
59
  return (
60
  <Card className="w-full max-w-6xl mx-auto">
61
  <CardHeader>
62
  <CardTitle>LLM Pricing Comparison Tool</CardTitle>
63
  </CardHeader>
64
  <CardContent>
65
+ <div className="mb-4">
66
  <h3 className="text-lg font-semibold mb-2">Select Comparison Models</h3>
67
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
68
+ {data.map((provider) => (
69
+ <Collapsible
70
+ key={provider.provider}
71
+ open={expandedProviders.includes(provider.provider)}
72
+ onOpenChange={() => toggleProviderExpansion(provider.provider)}
73
+ >
74
+ <CollapsibleTrigger asChild>
75
+ <Button variant="outline" className="w-full justify-between">
76
+ {provider.provider}
77
+ {expandedProviders.includes(provider.provider) ? (
78
+ <ChevronDown className="h-4 w-4" />
79
+ ) : (
80
+ <ChevronRight className="h-4 w-4" />
81
+ )}
82
+ </Button>
83
+ </CollapsibleTrigger>
84
+ <CollapsibleContent className="mt-2">
85
+ {provider.models.map((model) => (
86
+ <div key={`${provider.provider}:${model.name}`} className="flex items-center space-x-2 mb-1">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  <Checkbox
88
  id={`${provider.provider}:${model.name}`}
89
  checked={comparisonModels.includes(`${provider.provider}:${model.name}`)}
 
101
  htmlFor={`${provider.provider}:${model.name}`}
102
  className="text-sm font-medium text-gray-700"
103
  >
104
+ {model.name}
105
  </label>
106
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  ))}
108
+ </CollapsibleContent>
109
+ </Collapsible>
110
+ ))}
111
  </div>
112
+ </div>
113
 
114
+ <div className="flex gap-4 mb-4">
115
+ <div className="flex-1">
116
+ <label htmlFor="inputTokens" className="block text-sm font-medium text-gray-700">
117
+ Input Tokens (millions)
118
+ </label>
119
+ <Input
120
+ id="inputTokens"
121
+ type="number"
122
+ value={inputTokens}
123
+ onChange={(e) => setInputTokens(Number(e.target.value))}
124
+ className="mt-1"
125
+ />
126
+ </div>
127
+ <div className="flex-1">
128
+ <label htmlFor="outputTokens" className="block text-sm font-medium text-gray-700">
129
+ Output Tokens (millions)
130
+ </label>
131
+ <Input
132
+ id="outputTokens"
133
+ type="number"
134
+ value={outputTokens}
135
+ onChange={(e) => setOutputTokens(Number(e.target.value))}
136
+ className="mt-1"
137
+ />
138
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  </div>
140
+
141
+ <p className="italic text-sm text-muted-foreground mb-4">
142
+ Note: If you use Amazon Bedrock or Azure prices for Anthropic, Cohere or OpenAI should be the same.
143
+ </p>
144
+
145
+ <Table>
146
+ <TableHeader>
147
+ <TableRow>
148
+ <TableHead>
149
+ <Select onValueChange={(value) => setSelectedProviders(value as string[])} defaultValue={[]} multiple>
150
+ <SelectTrigger>
151
+ <SelectValue placeholder="Select Providers" />
152
+ </SelectTrigger>
153
+ <SelectContent>
154
+ {data.map((provider) => (
155
+ <SelectItem key={provider.provider} value={provider.provider}>
156
+ {provider.provider}
157
+ </SelectItem>
158
+ ))}
159
+ </SelectContent>
160
+ </Select>
161
+ </TableHead>
162
+ <TableHead>
163
+ <Select onValueChange={(value) => setSelectedModels(value as string[])} defaultValue={[]} multiple>
164
+ <SelectTrigger>
165
+ <SelectValue placeholder="Select Models" />
166
+ </SelectTrigger>
167
+ <SelectContent>
168
+ {data
169
+ .flatMap((provider) => provider.models)
170
+ .map((model) => (
171
+ <SelectItem key={model.name} value={model.name}>
172
+ {model.name}
173
+ </SelectItem>
174
+ ))}
175
+ </SelectContent>
176
+ </Select>
177
+ </TableHead>
178
+ <TableHead>Input Price (per 1M tokens)</TableHead>
179
+ <TableHead>Output Price (per 1M tokens)</TableHead>
180
+ <TableHead>Total Price</TableHead>
181
+ {comparisonModels.map((model) => (
182
+ <TableHead key={model} colSpan={2}>
183
+ Compared to {model}
184
+ </TableHead>
185
+ ))}
186
+ </TableRow>
187
+ <TableRow>
188
+ <TableHead />
189
+ <TableHead />
190
+ <TableHead />
191
+ <TableHead />
192
+ <TableHead />
193
+ {comparisonModels.flatMap((model) => [
194
+ <TableHead key={`${model}-input`}>Input</TableHead>,
195
+ <TableHead key={`${model}-output`}>Output</TableHead>,
196
+ ])}
197
+ </TableRow>
198
+ </TableHeader>
199
+ <TableBody>
200
+ {filteredData.flatMap((provider) =>
201
+ provider.models.map((model) => (
202
+ <TableRow key={`${provider.provider}-${model.name}`}>
203
+ <TableCell>{provider.provider}</TableCell>
204
+ <TableCell>{model.name}</TableCell>
205
+ <TableCell>${model.inputPrice.toFixed(2)}</TableCell>
206
+ <TableCell>${model.outputPrice.toFixed(2)}</TableCell>
207
+ <TableCell className="font-bold">
208
+ $
209
+ {(
210
+ calculatePrice(model.inputPrice, inputTokens) + calculatePrice(model.outputPrice, outputTokens)
211
+ ).toFixed(2)}
212
+ </TableCell>
213
+ {comparisonModels.flatMap((comparisonModel) => {
214
+ const [comparisonProvider, comparisonModelName] = comparisonModel.split(':')
215
+ const comparisonModelData = data
216
+ .find((p) => p.provider === comparisonProvider)
217
+ ?.models.find((m) => m.name === comparisonModelName)!
218
+ return [
219
+ <TableCell
220
+ key={`${comparisonModel}-input`}
221
+ className={`${
222
+ parseFloat(calculateComparison(model.inputPrice, comparisonModelData.inputPrice)) < 0
223
+ ? 'bg-green-100'
224
+ : parseFloat(calculateComparison(model.inputPrice, comparisonModelData.inputPrice)) > 0
225
+ ? 'bg-red-100'
226
+ : ''
227
+ }`}
228
+ >
229
+ {`${provider.provider}:${model.name}` === comparisonModel
230
+ ? '0.00%'
231
+ : `${calculateComparison(model.inputPrice, comparisonModelData.inputPrice)}%`}
232
+ </TableCell>,
233
+ <TableCell
234
+ key={`${comparisonModel}-output`}
235
+ className={`${
236
+ parseFloat(calculateComparison(model.outputPrice, comparisonModelData.outputPrice)) < 0
237
+ ? 'bg-green-100'
238
+ : parseFloat(calculateComparison(model.outputPrice, comparisonModelData.outputPrice)) > 0
239
+ ? 'bg-red-100'
240
+ : ''
241
+ }`}
242
+ >
243
+ {`${provider.provider}:${model.name}` === comparisonModel
244
+ ? '0.00%'
245
+ : `${calculateComparison(model.outputPrice, comparisonModelData.outputPrice)}%`}
246
+ </TableCell>,
247
+ ]
248
+ })}
249
+ </TableRow>
250
+ ))
251
+ )}
252
+ </TableBody>
253
+ </Table>
254
  </CardContent>
255
  </Card>
256
  )
src/components/ui/button.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
14
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
15
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
16
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
17
+ link: 'text-primary underline-offset-4 hover:underline',
18
+ },
19
+ size: {
20
+ default: 'h-10 px-4 py-2',
21
+ sm: 'h-9 rounded-md px-3',
22
+ lg: 'h-11 rounded-md px-8',
23
+ icon: 'h-10 w-10',
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: 'default',
28
+ size: 'default',
29
+ },
30
+ }
31
+ )
32
+
33
+ export interface ButtonProps
34
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
+ VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean
37
+ }
38
+
39
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
41
+ const Comp = asChild ? Slot : 'button'
42
+ return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
43
+ }
44
+ )
45
+ Button.displayName = 'Button'
46
+
47
+ export { Button, buttonVariants }
src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2
+
3
+ const Collapsible = CollapsiblePrimitive.Root
4
+
5
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
6
+
7
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
8
+
9
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }