Skip to content

Commit f64f153

Browse files
authored
Merge pull request #35 from jamesmcroft/image-presets
Add image presets
2 parents 1a50583 + a90a60d commit f64f153

File tree

9 files changed

+580
-347
lines changed

9 files changed

+580
-347
lines changed

src/App.jsx

Lines changed: 8 additions & 347 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,11 @@
1-
import { useState } from "react";
21
import { ThemeProvider } from "@mui/material/styles";
32
import theme from "./components/layout/Theme";
4-
import {
5-
Container,
6-
TextField,
7-
Button,
8-
Typography,
9-
MenuItem,
10-
Select,
11-
InputLabel,
12-
FormControl,
13-
IconButton,
14-
Grid2 as Grid,
15-
Accordion,
16-
AccordionSummary,
17-
AccordionDetails,
18-
Box,
19-
} from "@mui/material";
20-
import { Add, Delete, FileCopy, GitHub, LinkedIn } from "@mui/icons-material";
21-
import { useBoundStore } from "./stores";
3+
import Calculator from "./components/calculator/Calculator";
4+
import CalculationExplanation from "./components/calculator/CalculationExplanation";
5+
import Footer from "./components/layout/Footer";
6+
import { Container, Typography, Grid2 as Grid } from "@mui/material";
227

238
function App() {
24-
const [modelName, setModelName] = useState("");
25-
26-
const images = useBoundStore((state) => state.images);
27-
const models = useBoundStore((state) => state.models);
28-
const setModel = useBoundStore((state) => state.setModel);
29-
const addImage = useBoundStore((state) => state.addImage);
30-
const updateImage = useBoundStore((state) => state.updateImage);
31-
const removeImage = useBoundStore((state) => state.removeImage);
32-
const runCalculation = useBoundStore((state) => state.runCalculation);
33-
const resetCalculation = useBoundStore((state) => state.resetCalculation);
34-
const totalTokens = useBoundStore((state) => state.totalTokens);
35-
const totalCost = useBoundStore((state) => state.totalCost);
36-
const model = useBoundStore((state) => state.model);
37-
38-
const selectModel = (model) => {
39-
setModelName(model);
40-
41-
const selectedModel = models.find((m) => m.name === model);
42-
if (selectedModel) {
43-
setModel(selectedModel);
44-
runCalculation();
45-
} else {
46-
setModel(null);
47-
resetCalculation();
48-
}
49-
};
50-
51-
const addNewImage = () => {
52-
addImage({ height: 0, width: 0, multiplier: 1 });
53-
};
54-
55-
const cloneImage = (index) => {
56-
const image = images[index];
57-
addImage({
58-
height: image.height,
59-
width: image.width,
60-
multiplier: image.multiplier,
61-
});
62-
};
63-
64-
const handleSubmit = (event) => {
65-
event.preventDefault();
66-
runCalculation();
67-
};
68-
699
return (
7010
<ThemeProvider theme={theme}>
7111
<Container maxWidth="lg">
@@ -74,291 +14,12 @@ function App() {
7414
</Typography>
7515
<Grid container spacing={4}>
7616
<Grid size={{ xs: 12, md: 8 }}>
77-
<form onSubmit={handleSubmit}>
78-
<FormControl fullWidth margin="normal" required>
79-
<InputLabel id="model-label">Model</InputLabel>
80-
<Select
81-
labelId="model-label"
82-
value={modelName}
83-
onChange={(e) => selectModel(e.target.value)}
84-
label="Model"
85-
>
86-
<MenuItem value="">
87-
<em>None</em>
88-
</MenuItem>
89-
{models.map((model) => (
90-
<MenuItem key={model.name} value={model.name}>
91-
{model.name}
92-
</MenuItem>
93-
))}
94-
</Select>
95-
</FormControl>
96-
97-
{images.map((image, index) => (
98-
<Grid container spacing={1} alignItems="center" key={index}>
99-
<Grid size={{ sm: 4 }}>
100-
<TextField
101-
label="Image Height (px)"
102-
type="number"
103-
value={image.height}
104-
onChange={(e) =>
105-
updateImage(index, "height", e.target.value)
106-
}
107-
margin="normal"
108-
required
109-
fullWidth
110-
/>
111-
</Grid>
112-
<Grid size={{ sm: 4 }}>
113-
<TextField
114-
label="Image Width (px)"
115-
type="number"
116-
value={image.width}
117-
onChange={(e) =>
118-
updateImage(index, "width", e.target.value)
119-
}
120-
margin="normal"
121-
required
122-
fullWidth
123-
/>
124-
</Grid>
125-
<Grid size={{ sm: 2 }}>
126-
<TextField
127-
label="Count"
128-
type="number"
129-
value={image.multiplier}
130-
onChange={(e) =>
131-
updateImage(index, "multiplier", e.target.value)
132-
}
133-
margin="normal"
134-
required
135-
fullWidth
136-
/>
137-
</Grid>
138-
<Grid size={{ sm: 1 }}>
139-
<IconButton
140-
onClick={() => cloneImage(index)}
141-
color="primary"
142-
>
143-
<FileCopy />
144-
</IconButton>
145-
</Grid>
146-
<Grid size={{ sm: 1 }}>
147-
<IconButton
148-
onClick={() => removeImage(index)}
149-
color="secondary"
150-
>
151-
<Delete />
152-
</IconButton>
153-
</Grid>
154-
</Grid>
155-
))}
156-
157-
<Button
158-
onClick={addNewImage}
159-
variant="contained"
160-
color="info"
161-
style={{ marginBottom: "20px" }}
162-
fullWidth
163-
>
164-
<Add /> Add Image
165-
</Button>
166-
167-
<Button
168-
type="submit"
169-
variant="contained"
170-
color="primary"
171-
fullWidth
172-
>
173-
Calculate
174-
</Button>
175-
</form>
176-
177-
<Box mt={2}>
178-
{images.map((image, index) =>
179-
image.resizedHeight ? (
180-
<Box key={index} sx={{ display: "flex" }} mb={2}>
181-
<Box sx={{ flex: "1 0 auto" }}>
182-
<Typography variant="h6" gutterBottom>
183-
Image {index + 1}
184-
</Typography>
185-
<Box
186-
sx={{
187-
display: "flex",
188-
alignItems: "center",
189-
}}
190-
>
191-
<Box sx={{ flexGrow: 1 }}>
192-
<Typography variant="body2" color="textSecondary">
193-
Resized Size
194-
</Typography>
195-
<Typography variant="body1" gutterBottom>
196-
{image.resizedHeight} x {image.resizedWidth}
197-
</Typography>
198-
</Box>
199-
<Box sx={{ flexGrow: 1 }}>
200-
<Typography variant="body2" color="textSecondary">
201-
Tiles (per image)
202-
</Typography>
203-
<Typography variant="body1" gutterBottom>
204-
{image.tilesHigh} x {image.tilesWide}
205-
</Typography>
206-
</Box>
207-
<Box sx={{ flexGrow: 1 }}>
208-
<Typography variant="body2" color="textSecondary">
209-
Total tiles
210-
</Typography>
211-
<Typography variant="body1" gutterBottom>
212-
{image.totalTiles}
213-
</Typography>
214-
</Box>
215-
</Box>
216-
</Box>
217-
</Box>
218-
) : null
219-
)}
220-
221-
{totalTokens !== null && (
222-
<Box sx={{ display: "flex" }} mb={2}>
223-
<Box sx={{ flex: "1 0 auto" }}>
224-
<Typography variant="h6" gutterBottom>
225-
Result
226-
</Typography>
227-
<Box
228-
sx={{
229-
display: "flex",
230-
alignItems: "center",
231-
}}
232-
>
233-
{model && (
234-
<Box sx={{ flexGrow: 1 }}>
235-
<Typography variant="body2" color="textSecondary">
236-
Base tokens
237-
</Typography>
238-
<Typography variant="body1" gutterBottom>
239-
{model.baseTokens}
240-
</Typography>
241-
</Box>
242-
)}
243-
244-
{model && (
245-
<Box sx={{ flexGrow: 1 }}>
246-
<Typography variant="body2" color="textSecondary">
247-
Tile tokens
248-
</Typography>
249-
<Typography variant="body1" gutterBottom>
250-
{model.tokensPerTile} x{" "}
251-
{images
252-
.map((image) => image.totalTiles ?? 0)
253-
.reduce((acc, val) => acc + val, 0)}{" "}
254-
= {totalTokens - model.baseTokens}
255-
</Typography>
256-
</Box>
257-
)}
258-
259-
{totalTokens !== null && (
260-
<Box sx={{ flexGrow: 1 }}>
261-
<Typography variant="body2" color="textSecondary">
262-
Total tokens
263-
</Typography>
264-
<Typography variant="body1" gutterBottom>
265-
{totalTokens}
266-
</Typography>
267-
</Box>
268-
)}
269-
270-
{totalCost !== null && (
271-
<Box sx={{ flexGrow: 1 }}>
272-
<Typography variant="body2" color="textSecondary">
273-
Total cost
274-
</Typography>
275-
<Typography variant="body1" gutterBottom>
276-
${totalCost}
277-
</Typography>
278-
</Box>
279-
)}
280-
</Box>
281-
</Box>
282-
</Box>
283-
)}
284-
</Box>
17+
<Calculator />
28518
</Grid>
286-
<Grid size={{ xs: 12, md: 4 }}>
287-
<Accordion expanded>
288-
<AccordionSummary
289-
aria-controls="calculation-explanation-content"
290-
id="calculation-explanation-header"
291-
>
292-
<Typography variant="h6">How the Calculation Works</Typography>
293-
</AccordionSummary>
294-
{model ? (
295-
<AccordionDetails>
296-
<Typography>
297-
<p>The calculation involves several steps:</p>
298-
</Typography>
299-
<Typography>
300-
1. <b>Resizing Images</b>: Ensure each image is resized to
301-
fit within the maximum dimension {model.maxImageDimension}
302-
px, and has at least {model.imageMinSizeLength}px on the
303-
shortest side, while maintaining its aspect ratio.
304-
</Typography>
305-
<Typography>
306-
2. <b>Calculating Tiles</b>: The resized image is divided
307-
into tiles based on the model's tile size of{" "}
308-
{model.tileSizeLength}px by {model.tileSizeLength}px.
309-
</Typography>
310-
<Typography>
311-
3. <b>Token Calculation</b>: The total number of tokens is
312-
calculated by multiplying the number of tiles by the tokens
313-
per tile ({model.tokensPerTile}) and adding{" "}
314-
{model.baseTokens} base tokens.
315-
</Typography>
316-
<Typography>
317-
4. <b>Cost Calculation</b>: The total cost is calculated
318-
based on the total number of tokens and the cost per
319-
thousand tokens (${model.costPerThousandTokens}).
320-
</Typography>
321-
</AccordionDetails>
322-
) : (
323-
<AccordionDetails>
324-
<Typography>
325-
Please select a model to see the calculation explanation.
326-
</Typography>
327-
</AccordionDetails>
328-
)}
329-
</Accordion>
33019

331-
<Box mt={4} textAlign="center">
332-
<Typography variant="body2">Created by James Croft.</Typography>
333-
</Box>
334-
<Box
335-
sx={{
336-
display: "flex",
337-
flexDirection: "column",
338-
mt: 2,
339-
gap: 1,
340-
}}
341-
>
342-
<Button
343-
fullWidth
344-
variant="outlined"
345-
color="dark"
346-
startIcon={<GitHub />}
347-
href="https://github.com/jamesmcroft/openai-image-token-calculator"
348-
target="_blank"
349-
>
350-
View Source on GitHub
351-
</Button>
352-
<Button
353-
variant="outlined"
354-
color="primary"
355-
startIcon={<LinkedIn />}
356-
href="https://www.linkedin.com/in/jmcroft"
357-
target="_blank"
358-
>
359-
View Profile on LinkedIn
360-
</Button>
361-
</Box>
20+
<Grid size={{ xs: 12, md: 4 }}>
21+
<CalculationExplanation />
22+
<Footer />
36223
</Grid>
36324
</Grid>
36425
</Container>

0 commit comments

Comments
 (0)