Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,19 @@ The testing related `.spec.tsx` files are used with Playwright for browser tests
## Running Tests

### Unit Tests (Jest)

Run Jest unit tests:

```bash
npm run test
```

### Integration Tests (Playwright)

#### Local Testing

Run Playwright tests locally (requires local dev server):

```bash
npm run test:pw:local
```
Expand Down
99 changes: 99 additions & 0 deletions src/components/ProjectForm/GeminiConfigForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from "react";
import { Link } from "@mui/material";
import { TextValidator } from "react-material-ui-form-validator";
import { GeminiVlmConfig } from "../../types/imageComparison";
import { Tooltip } from "../Tooltip";
import { useConfigHook } from "./useConfigHook";
import {
useProjectState,
useProjectDispatch,
setProjectEditState,
} from "../../contexts";
import {
VlmPromptField,
VlmTemperatureField,
VlmUseThinkingField,
} from "./VlmSharedFields";

export const GeminiConfigForm: React.FunctionComponent = () => {
const [config, updateConfig] = useConfigHook<GeminiVlmConfig>();
const { projectEditState: project } = useProjectState();
const projectDispatch = useProjectDispatch();

return (
<React.Fragment>
<Tooltip title="The Google Gemini model to use for image comparison.">
<div>
<TextValidator
name="model"
validators={["required"]}
errorMessages={["Model is required"]}
margin="dense"
id="model"
label="Gemini Model"
type="text"
fullWidth
required
value={config.model || ""}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
updateConfig("model", event.target.value);
}}
helperText={
<span>
Gemini model name.{" "}
<Link
href="https://ai.google.dev/models/gemini"
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
View all available models
</Link>
</span>
}
/>
</div>
</Tooltip>
<Tooltip title="Your Google Gemini API key. Get one from https://makersuite.google.com/app/apikey">
<div>
<TextValidator
name="apiKey"
validators={["required"]}
errorMessages={["API key is required"]}
margin="dense"
id="apiKey"
label="Gemini API Key"
type="password"
fullWidth
required
value={config.apiKey || ""}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const updatedConfig: GeminiVlmConfig = {
...config,
apiKey: event.target.value,
};
setProjectEditState(projectDispatch, {
...project,
imageComparisonConfig: JSON.stringify(updatedConfig),
});
}}
helperText="Enter your Google Gemini API key"
/>
</div>
</Tooltip>
<VlmPromptField
value={config.prompt}
onChange={(value) => updateConfig("prompt", value)}
/>
<VlmTemperatureField
value={config.temperature}
onChange={(value) => updateConfig("temperature", value)}
/>
<VlmUseThinkingField
value={config.useThinking || false}
onChange={(value) => updateConfig("useThinking", value)}
/>
</React.Fragment>
);
};

170 changes: 170 additions & 0 deletions src/components/ProjectForm/OllamaConfigForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import React, { useState, useEffect, useMemo } from "react";
import {
LinearProgress,
FormControl,
InputLabel,
Select,
MenuItem,
FormHelperText,
SelectChangeEvent,
} from "@mui/material";
import { TextValidator } from "react-material-ui-form-validator";
import { useSnackbar } from "notistack";
import { OllamaVlmConfig } from "../../types/imageComparison";
import { Tooltip } from "../Tooltip";
import { useConfigHook } from "./useConfigHook";
import { ollamaService } from "../../services";
import { OllamaModel } from "../../types";
import {
VlmPromptField,
VlmTemperatureField,
VlmUseThinkingField,
} from "./VlmSharedFields";

export const OllamaConfigForm: React.FunctionComponent = () => {
const { enqueueSnackbar } = useSnackbar();
const [config, updateConfig] = useConfigHook<OllamaVlmConfig>();
const [models, setModels] = useState<OllamaModel[]>([]);
const [loading, setLoading] = useState<boolean>(false);

useEffect(() => {
let isMounted = true;
setLoading(true);
ollamaService
.listModels()
.then((fetchedModels) => {
if (isMounted) {
setModels(fetchedModels);
}
})
.catch((err) => {
if (isMounted) {
enqueueSnackbar(err, {
variant: "error",
});
}
})
.finally(() => {
if (isMounted) {
setLoading(false);
}
});
return () => {
isMounted = false;
};
}, [enqueueSnackbar]);

const hasError = useMemo(() => {
if (!config.model || loading || models.length === 0) {
return false;
}
return !models.some((m) => m.name === config.model);
}, [config.model, models, loading]);

const handleModelChange = (event: SelectChangeEvent<string>) => {
updateConfig("model", event.target.value);
};

const renderModelField = () => {
if (loading) {
return (
<TextValidator
name="model"
validators={["required"]}
errorMessages={["Model is required"]}
margin="dense"
id="model"
label="Model"
type="text"
fullWidth
required
value={config.model || ""}
disabled
helperText="Loading models..."
/>
);
}

if (models.length === 0) {
return (
<TextValidator
name="model"
validators={["required"]}
errorMessages={["Model is required"]}
margin="dense"
id="model"
label="Model"
type="text"
fullWidth
required
value={config.model || ""}
helperText="No models available. Enter model name manually."
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
updateConfig("model", event.target.value);
}}
/>
);
}

return (
<FormControl
variant="standard"
fullWidth
margin="dense"
error={hasError}
required
>
<InputLabel id="model-select-label">Model</InputLabel>
<Select
labelId="model-select-label"
id="model-select"
value={config.model || ""}
onChange={handleModelChange}
name="model"
>
{models.map((model) => (
<MenuItem key={model.name} value={model.name}>
{model.name}
</MenuItem>
))}
</Select>
{hasError && (
<FormHelperText>
Selected model is not in the available list.
</FormHelperText>
)}
</FormControl>
);
};

return (
<React.Fragment>
<Tooltip title="The Ollama model name to use for image comparison. Models are fetched from the Ollama service.">
<div>
{renderModelField()}
<TextValidator
name="model"
validators={["required"]}
errorMessages={["Model is required"]}
value={config.model || ""}
style={{ display: "none" }}
/>
</div>
</Tooltip>
{loading && <LinearProgress />}
<VlmPromptField
value={config.prompt}
onChange={(value) => updateConfig("prompt", value)}
/>
<VlmTemperatureField
value={config.temperature}
onChange={(value) => updateConfig("temperature", value)}
/>
<VlmUseThinkingField
value={config.useThinking || false}
onChange={(value) => updateConfig("useThinking", value)}
/>
</React.Fragment>
);
};

4 changes: 1 addition & 3 deletions src/components/ProjectForm/ProjectForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,7 @@ export const ProjectForm: React.FunctionComponent = () => {
<MenuItem value={ImageComparison.odiff}>
{ImageComparison.odiff}
</MenuItem>
<MenuItem value={ImageComparison.vlm}>
{ImageComparison.vlm}
</MenuItem>
<MenuItem value={ImageComparison.vlm}>{ImageComparison.vlm}</MenuItem>
</Select>
</FormControl>
{config}
Expand Down
Loading