From 3e0d8379dd7b112df2af55d434da1400bd858e49 Mon Sep 17 00:00:00 2001 From: Aleksi Huotala <7612995+alehuo@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:39:25 +0200 Subject: [PATCH 01/15] Retrieve papers from Redux store --- client/src/pages/ProjectPage.tsx | 54 ++++++++++---------------------- 1 file changed, 16 insertions(+), 38 deletions(-) diff --git a/client/src/pages/ProjectPage.tsx b/client/src/pages/ProjectPage.tsx index 704280e..a8b4234 100644 --- a/client/src/pages/ProjectPage.tsx +++ b/client/src/pages/ProjectPage.tsx @@ -7,10 +7,7 @@ import { DropdownMenuText, DropdownOption } from "../components/DropDownMenus"; import { FileDropArea } from "../components/FileDropArea"; import { ExpandableToast } from "../components/ExpandableToast"; import { TruncatedFileNames } from "../components/TruncatedFileNames"; -import { - fetchJobTasksFromBackend, - fetchPapersFromBackend, -} from "../services/jobTaskService"; +import { fetchJobTasksFromBackend } from "../services/jobTaskService"; import { createJob, fetchJobsForProject } from "../services/jobService"; import { fileUploadToBackend, @@ -22,7 +19,6 @@ import { FetchedFile, JobTask, JobTaskStatus, - Paper, CreatedJob, LlmConfig, createZeroShotPromptingConfig, @@ -164,8 +160,10 @@ export const ProjectPage = () => { const [isLlmProviderSelected, setIsLlmProviderSelected] = useState(false); const [modelsLoaded, setModelsLoaded] = useState(false); const [isLlmSelected, setIsLlmSelected] = useState(false); - const [papersLoading, setPapersLoading] = useState(false); - const [papers, setPapers] = useState([]); + + const getPapers = useTypedStoreState((state) => state.getPapersForProject); + const papers = getPapers(projectUuid); + const [createdJobs, setCreatedJobs] = useState([]); const [fetchedFiles, setFetchedFiles] = useState([]); const [jobTasks, setJobTasks] = useState([]); @@ -324,23 +322,6 @@ export const ProjectPage = () => { const currentTaskUuid = paperUuid ? paperToTaskMap[paperUuid] : undefined; - const loadPapers = useCallback(async () => { - setPapersLoading(true); - try { - const fetched = await fetchPapersFromBackend(projectUuid); - // console.log("Fetched papers", fetched); - setPapers(fetched); - } catch (e) { - console.error("Failed to fetch papers", e); - } finally { - setPapersLoading(false); - } - }, [projectUuid]); - - useEffect(() => { - loadPapers(); - }, [loadPapers]); - const createZeroShotJob = useCallback(async () => { if (!selectedLlm) { toast.error("Please select a model before creating a task."); @@ -370,19 +351,12 @@ export const ProjectPage = () => { updated_at: res.updated_at, }; setCreatedJobs((prev) => [...prev, createdJob]); - await loadPapers(); + // await loadPapers(); } catch (e) { console.error("Error creating job:", e); toast.error("Error creating job"); } - }, [ - selectedLlm, - selectedLlmProvider, - modelFormValues, - providerFormValues, - projectUuid, - loadPapers, - ]); + }, [selectedLlm, selectedLlmProvider, modelFormValues, providerFormValues, projectUuid]); const uploadFilesToBackend = useCallback( async (files: File[]) => { @@ -424,12 +398,16 @@ export const ProjectPage = () => { try { await uploadFilesToBackend(files); await fetchFiles(); - await loadPapers(); + // await loadPapers(); } catch (error) { console.error("Problem uploading the files", error); } }, - [uploadFilesToBackend, fetchFiles, loadPapers] + [ + uploadFilesToBackend, + fetchFiles, + // loadPapers + ] ); useEffect(() => { @@ -493,7 +471,7 @@ export const ProjectPage = () => { } } } - await loadPapers(); + // await loadPapers(); navigate(`/project/${projectUuid}`); toast.success("Manual evaluation finished."); }, [ @@ -503,7 +481,7 @@ export const ProjectPage = () => { paperToTaskMap, navigate, projectUuid, - loadPapers, + // loadPapers, ]); useEffect(() => { @@ -974,7 +952,7 @@ export const ProjectPage = () => { variant="green" className="px-6 text-md font-bold rounded-lg " onClick={openManualEvaluation} - disabled={papersLoading || !canStartManualEvaluation} + disabled={!canStartManualEvaluation} >
From 899a1405c7393437649158f54ac99a031440bc90 Mon Sep 17 00:00:00 2001 From: Aleksi Huotala <7612995+alehuo@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:27:00 +0200 Subject: [PATCH 02/15] Start app in every environment on port 3000 --- README.md | 2 +- start-dev.bat | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9acec7c..8e3c64e 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ After startup, open the application: If you ran `start-prod`, navigate to [https://localhost](https://localhost) (the Caddy server's root CA is by default untrusted. You can bypass the browser warning). -If you used `make start-dev`, navigate to [http://localhost:3001](http://localhost:3001) +If you used `make start-dev`, navigate to [http://localhost:3000](http://localhost:3000) #### Windows (non-WSL) diff --git a/start-dev.bat b/start-dev.bat index db604cc..4b4000d 100644 --- a/start-dev.bat +++ b/start-dev.bat @@ -1,6 +1,6 @@ -set FRONTEND_PORT=3001 -set FLOWER_PORT=5556 -set ADMINER_PORT=8081 +set FRONTEND_PORT=3000 +set FLOWER_PORT=5555 +set ADMINER_PORT=8080 set APP_ENV=dev docker compose -f docker-compose.yml -p dev up --build \ No newline at end of file From b66c8e002022246d098d338de3293e6ad22cebc2 Mon Sep 17 00:00:00 2001 From: Aleksi Huotala <7612995+alehuo@users.noreply.github.com> Date: Tue, 11 Nov 2025 08:04:12 +0200 Subject: [PATCH 03/15] Add TODO notes regarding unimplemented features --- client/src/components/ManualEvaluationModal.tsx | 3 +++ client/src/pages/ProjectPage.tsx | 5 ++++- client/src/state/store.ts | 10 +++++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/client/src/components/ManualEvaluationModal.tsx b/client/src/components/ManualEvaluationModal.tsx index 529be4c..70646bd 100644 --- a/client/src/components/ManualEvaluationModal.tsx +++ b/client/src/components/ManualEvaluationModal.tsx @@ -47,10 +47,12 @@ export const ManualEvaluationModal: React.FC = ({ }) => { const currentPaper = papers.find((p) => p.uuid === paperUuid); + // TODO: Refactor this to use redux const [modelSuggestions, setModelSuggestions] = useState( [], ); + // TODO: Refactor this to use redux const addHumanResult = useCallback( async (humanResult: JobTaskHumanResult) => { if (!paperUuid) return; @@ -64,6 +66,7 @@ export const ManualEvaluationModal: React.FC = ({ [paperUuid, onEvaluated], ); + // TODO: Refactor this to use Redux const getModelSuggestions = useCallback(async (paperUuid: string) => { const response = await axios.get(`/api/v1/jobtask?paper_uuid=${paperUuid}`); /* eslint-disable @typescript-eslint/no-explicit-any */ diff --git a/client/src/pages/ProjectPage.tsx b/client/src/pages/ProjectPage.tsx index a8b4234..dd349c7 100644 --- a/client/src/pages/ProjectPage.tsx +++ b/client/src/pages/ProjectPage.tsx @@ -184,7 +184,9 @@ export const ProjectPage = () => { if (project !== undefined) { fetchPapers(projectUuid); } - }, [fetchPapers, project, projectUuid]); + fetchModels(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [project, projectUuid]); const paperUuid = useMemo(() => { if (!search) return null; @@ -194,6 +196,7 @@ export const ProjectPage = () => { const [selectedLlmProvider, setSelectedLlmProvider] = useState< DropdownOption | undefined >(undefined); + const [selectedLlm, setSelectedLlm] = useState( undefined ); diff --git a/client/src/state/store.ts b/client/src/state/store.ts index 70f136d..e3b2912 100644 --- a/client/src/state/store.ts +++ b/client/src/state/store.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { createStore, action, @@ -15,7 +16,7 @@ import { JobTaskHumanResult, PaperWithModelEval, Project, - Provider + Provider, } from "./types"; const injections = { @@ -36,7 +37,6 @@ type ProjectUUID = string; // Defines state, actions and thunks for project-related things. interface ProjectModel { - // Projects projects: Array; setProjects: Action>; setLoadingProjects: Action; @@ -167,7 +167,7 @@ export const store = createStore( const { paperService } = injections; await paperService.addPaperHumanResult( params.paperUuid, - params.humanResult + params.humanResult, ); actions.setPaperHumanResult({ projectUuid: params.projectUuid, @@ -209,7 +209,7 @@ export const store = createStore( getPaperByUuid: computed((state) => { return (projectUuid: string, paperUuid: string) => (state.papers[projectUuid] || []).find( - (paper) => paper.uuid === paperUuid + (paper) => paper.uuid === paperUuid, ); }), providers: [], @@ -243,7 +243,7 @@ export const store = createStore( { injections, devTools: process.env.NODE_ENV !== "production", - } + }, ); const typedHooks = createTypedHooks(); From 7af2861f18c42ad45107bfaf0e7d747ae137b033 Mon Sep 17 00:00:00 2001 From: Aleksi Huotala <7612995+alehuo@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:19:35 +0200 Subject: [PATCH 04/15] Remove console.log --- client/src/pages/ProjectPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/ProjectPage.tsx b/client/src/pages/ProjectPage.tsx index dd349c7..2a0a78e 100644 --- a/client/src/pages/ProjectPage.tsx +++ b/client/src/pages/ProjectPage.tsx @@ -543,7 +543,7 @@ export const ProjectPage = () => { const inclusionCriteria = project?.criteria.inclusion_criteria; const exclusionCriteria = project?.criteria.exclusion_criteria; - console.log(providerFormValues); + return ( Date: Fri, 16 Jan 2026 20:18:12 +0200 Subject: [PATCH 05/15] Fix styling --- client/src/components/EventStream.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/src/components/EventStream.tsx b/client/src/components/EventStream.tsx index c83969b..2ba295e 100644 --- a/client/src/components/EventStream.tsx +++ b/client/src/components/EventStream.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { CircleAlert } from "lucide-react"; import * as z from "zod"; +import classNames from "classnames"; const EventName = { // Events for JobTask-related things @@ -39,7 +40,14 @@ type EventData = z.infer; const EventDataList: React.FC<{ logs: Array }> = ({ logs }) => { const [open, setOpen] = useState(false); return ( -
+
{!open && ( - )} -
- {!loading && setting !== null && !editMode && ( - <> - +
+ {!loading && !editMode && ( +
+ {setting !== null ? ( + <> +
+ + +
+ + + + ) : ( - - )} - {!loading && editMode && ( - <> + )} +
+ )} + {!loading && editMode && ( +
+
+ { - setValue(e.target.value); - e.preventDefault(); - }} + onChange={(e) => setValue(e.target.value)} /> +
+
- - )} -
+
+
+ )}
- +
); }; const ConfigParameterSchema = z.object({ key: z.string(), title: z.string(), + description: z.string().nullable(), type: z.enum(["string", "number", "boolean"]).default("string"), defaultValue: z .union([z.string(), z.number(), z.boolean()]) @@ -137,6 +195,7 @@ const ConfigParameterSchema = z.object({ const ProviderConfigParamsResponseSchema = z.object({ title: z.string(), + description: z.string(), config_parameters: z.array(ConfigParameterSchema), }); @@ -144,46 +203,65 @@ const ProviderConfigParamsMapSchema = z.record( z.string(), ProviderConfigParamsResponseSchema, ); - type ProviderConfigParamsMap = z.infer; export const SettingsPage = () => { const [entries, setEntries] = useState({}); + useEffect(() => { fetch("/api/v1/llm/provider_config_params") - .then((res) => { - return res.json().then((jsonData) => { - const contents = ProviderConfigParamsMapSchema.parse(jsonData); - setEntries(contents); - }); + .then((res) => res.json()) + .then((jsonData) => { + const contents = ProviderConfigParamsMapSchema.parse(jsonData); + setEntries(contents); }) .catch(); }, []); + + const providerKeys = useMemo(() => Object.keys(entries), [entries]); + return ( - {Object.keys(entries).map((key) => { - const entry = entries[key]; - if (entry.config_parameters.length === 0) { - return null; - } - return ( -
-

{entry.title}

-
- {entry.config_parameters.map((setting) => { - return ( +
+

Settings

+

+ Manage settings related to AiSysRev or LLM providers. +

+
+
+ {providerKeys.map((key) => { + const entry = entries[key]; + if (!entry || entry.config_parameters.length === 0) return null; + + return ( +
+
+
+

{entry.title}

+

+ {entry.description} +

+
+
+ +
+ {entry.config_parameters.map((setting) => ( - ); - })} -
-
- ); - })} + ))} +
+ + ); + })} +
); diff --git a/server/src/api/controllers/llm.py b/server/src/api/controllers/llm.py index de91389..f5d549a 100644 --- a/server/src/api/controllers/llm.py +++ b/server/src/api/controllers/llm.py @@ -33,6 +33,7 @@ async def get_providers() -> list[Provider]: class ProviderConfigParamsResponse(BaseModel): title: str + description: str config_parameters: List[ConfigParameter] @@ -45,6 +46,7 @@ async def get_provider_config_params(): return { provider.provider_name: ProviderConfigParamsResponse( title=provider.provider_title, + description=provider.provider_description, config_parameters=provider.config_parameters, ) for provider in llm_providers diff --git a/server/src/core/llm/providers/openai.py b/server/src/core/llm/providers/openai.py index af4fc35..e0a1e4b 100644 --- a/server/src/core/llm/providers/openai.py +++ b/server/src/core/llm/providers/openai.py @@ -33,7 +33,9 @@ def __init__( model_parameters_schema = OpenAIModelParams api_key_config_parameter = ConfigParameter( - key="openai_api_key", title="OpenAI API key" + key="openai_api_key", + title="OpenAI API key", + description="The OpenAI API requires an API key for model access.", ) config_parameters = [api_key_config_parameter] diff --git a/server/src/core/llm/providers/openrouter.py b/server/src/core/llm/providers/openrouter.py index 88af637..bce6d03 100644 --- a/server/src/core/llm/providers/openrouter.py +++ b/server/src/core/llm/providers/openrouter.py @@ -38,7 +38,9 @@ def __init__( model_parameters_schema = OpenRouterModelParams api_key_config_parameter = ConfigParameter( - key="openrouter_api_key", title="OpenRouter API key" + key="openrouter_api_key", + title="OpenRouter API key", + description="OpenRouter API key is used to authenticate requests to the OpenRouter API.", ) config_parameters = [api_key_config_parameter] diff --git a/server/src/core/llm/providers/provider.py b/server/src/core/llm/providers/provider.py index 4406e04..aa31a86 100644 --- a/server/src/core/llm/providers/provider.py +++ b/server/src/core/llm/providers/provider.py @@ -36,6 +36,7 @@ class ConfigParameter(BaseModel): key: str title: str + description: Optional[str] = None type: Literal["string", "number", "boolean"] = "string" defaultValue: Optional[Union[str, int, float, bool]] = None secret: bool = True From c275b629524d9d58e69e91746a11dd842e3e2b21 Mon Sep 17 00:00:00 2001 From: Aleksi Huotala <7612995+alehuo@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:02:27 +0200 Subject: [PATCH 10/15] Clean up task creation UI --- client/src/components/Card.tsx | 6 +- client/src/components/DropDownMenus.tsx | 9 +- client/src/pages/ProjectPage.tsx | 294 +++++++++++++++--------- 3 files changed, 187 insertions(+), 122 deletions(-) diff --git a/client/src/components/Card.tsx b/client/src/components/Card.tsx index f5b9d90..9fa7814 100644 --- a/client/src/components/Card.tsx +++ b/client/src/components/Card.tsx @@ -16,7 +16,7 @@ export const Card: React.FC> = ({
> = ({ "bg-yellow-200": variant == "warning", }, padding, - className - ) + className, + ), )} {...rest} > diff --git a/client/src/components/DropDownMenus.tsx b/client/src/components/DropDownMenus.tsx index 6206721..0eebcea 100644 --- a/client/src/components/DropDownMenus.tsx +++ b/client/src/components/DropDownMenus.tsx @@ -70,19 +70,18 @@ export const DropdownMenuText: React.FC = ({ {selected?.name || "-"} {options.map((option) => ( unknown; }; +type ModelConfigurationProps = { + isLlmSelected: boolean; + modelParametersSchema?: Provider["model_parameters_json_schema"]; + modelFormValues: Record; + setModelFormValue: React.Dispatch< + React.SetStateAction> + >; +}; + +const ModelConfiguration: React.FC = ({ + isLlmSelected, + modelParametersSchema, + modelFormValues, + setModelFormValue, +}) => { + return isLlmSelected && modelParametersSchema ? ( +
+ {Object.keys(modelParametersSchema.properties).map((key) => { + const property = modelParametersSchema.properties[key]; + return ( +
+

+ {property.title}{" "} + {modelFormValues[key] !== undefined && modelFormValues[key] !== "" + ? "(" + modelFormValues[key] + ")" + : ""} +

+

{property.description}

+ {property.type === "number" && ( + { + setModelFormValue((vals) => ({ + ...vals, + [key]: e.target.value, + })); + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Ok + value={modelFormValues[key]} + /> + )} + {property.type === "integer" && ( + { + setModelFormValue((vals) => ({ + ...vals, + [key]: e.target.value, + })); + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Ok + value={modelFormValues[key]} + /> + )} +
+ ); + })} +
+ ) : null; +}; + const ActionComponent: React.FC = ({ hasPapers, projectUuid, @@ -77,7 +150,7 @@ const ActionComponent: React.FC = ({ {hasPapers && (
{title}
@@ -160,6 +233,7 @@ export const ProjectPage = () => { const [isLlmProviderSelected, setIsLlmProviderSelected] = useState(false); const [modelsLoaded, setModelsLoaded] = useState(false); const [isLlmSelected, setIsLlmSelected] = useState(false); + const [promptingStrategy, setPromptingStrategy] = useState<"ZS" | "FS">("ZS"); const getPapers = useTypedStoreState((state) => state.getPapersForProject); const papers = getPapers(projectUuid); @@ -173,7 +247,7 @@ export const ProjectPage = () => { const loadingProjects = useTypedStoreState((state) => state.loading.projects); const loadProjects = useTypedStoreActions((actions) => actions.fetchProjects); const getProjectByUuid = useTypedStoreState( - (state) => state.getProjectByUuid + (state) => state.getProjectByUuid, ); const providers = useTypedStoreState((state) => state.providers); const fetchPapers = useTypedStoreActions((actions) => actions.fetchPapers); @@ -185,7 +259,7 @@ export const ProjectPage = () => { fetchPapers(projectUuid); } fetchModels(); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [project, projectUuid]); const paperUuid = useMemo(() => { @@ -198,7 +272,7 @@ export const ProjectPage = () => { >(undefined); const [selectedLlm, setSelectedLlm] = useState( - undefined + undefined, ); const provider = providers.find((p) => p.name === selectedLlmProvider?.value); @@ -217,7 +291,7 @@ export const ProjectPage = () => { [curr]: defaultVal === undefined ? undefined : defaultVal, }; }, - {} + {}, ); }, [providerParametersSchema]); const defaultModelValues = useMemo(() => { @@ -232,7 +306,7 @@ export const ProjectPage = () => { [curr]: defaultVal === undefined ? undefined : defaultVal, }; }, - {} + {}, ); }, [modelParametersSchema]); @@ -252,7 +326,7 @@ export const ProjectPage = () => { const pendingTasks = useMemo( () => papers.filter((paper) => paper.human_result == null), - [papers] + [papers], ); const evaluationFinished = jobTasks.length > 0 && pendingTasks.length === 0; @@ -280,7 +354,7 @@ export const ProjectPage = () => { setModelsLoaded(false); const models = await retrieve_models( selectedLlmProvider.value, - providerFormValues + providerFormValues, ); setAvailableModels(models); setModelsLoaded(true); @@ -288,7 +362,7 @@ export const ProjectPage = () => { console.error( "Failed to fetch available models for provider " + selectedLlmProvider.value, - error + error, ); } } @@ -359,7 +433,13 @@ export const ProjectPage = () => { console.error("Error creating job:", e); toast.error("Error creating job"); } - }, [selectedLlm, selectedLlmProvider, modelFormValues, providerFormValues, projectUuid]); + }, [ + selectedLlm, + selectedLlmProvider, + modelFormValues, + providerFormValues, + projectUuid, + ]); const uploadFilesToBackend = useCallback( async (files: File[]) => { @@ -382,7 +462,7 @@ export const ProjectPage = () => { throw e; } }, - [projectUuid] + [projectUuid], ); const fetchFiles = useCallback(async () => { @@ -410,7 +490,7 @@ export const ProjectPage = () => { uploadFilesToBackend, fetchFiles, // loadPapers - ] + ], ); useEffect(() => { @@ -432,7 +512,7 @@ export const ProjectPage = () => { // console.log("job.uuid", job.uuid); // @ts-expect-error Expected return fetchJobTasksFromBackend(job.uuid, job.id); - }) + }), ) .then((results) => { setJobTasks(results.flat()); @@ -468,7 +548,7 @@ export const ProjectPage = () => { const candidate = papers[i]; if (jobTasks.length === 0 || paperToTaskMap[candidate.uuid]) { navigate( - `/project/${projectUuid}/evaluate?paperUuid=${candidate.uuid}` + `/project/${projectUuid}/evaluate?paperUuid=${candidate.uuid}`, ); return; } @@ -517,7 +597,7 @@ export const ProjectPage = () => { const response = await fetch( `/api/v1/result/download_result_csv?${new URLSearchParams({ project_uuid: projectUuid, - }).toString()}` + }).toString()}`, ); if (!response.ok) { return; @@ -537,6 +617,12 @@ export const ProjectPage = () => { const hasPapers = papers && papers.length > 0; + useEffect(() => { + if (isLlmProviderSelected && !modelsLoaded) { + fetchModels(); + } + }, [fetchModels, isLlmProviderSelected, modelsLoaded]); + if (!project) { return ; } @@ -571,10 +657,10 @@ export const ProjectPage = () => { {createdJobs.map((job) => { const tasks = jobTasks.filter((task) => task.job_uuid === job.uuid); const doneCount = tasks.filter( - (task) => task.status === JobTaskStatus.DONE + (task) => task.status === JobTaskStatus.DONE, ).length; const errorCount = tasks.filter( - (task) => task.status === JobTaskStatus.ERROR + (task) => task.status === JobTaskStatus.ERROR, ).length; const totalCount = tasks.length; const completedCount = doneCount + errorCount; @@ -614,7 +700,7 @@ export const ProjectPage = () => { progress < 100, "[&::-webkit-progress-value]:bg-green-400": progress === 100, - } + }, )} /> )} @@ -682,7 +768,9 @@ export const ProjectPage = () => {
)}
-

Provider

+ ({ @@ -701,7 +789,7 @@ export const ProjectPage = () => { setSelected={setIsLlmProviderSelected} /> {/* {isLlmProviderSelected && ( -
+
@@ -716,7 +804,6 @@ export const ProjectPage = () => {
)} */} {isLlmProviderSelected && providerParametersSchema && ( - //
<> {Object.keys(providerParametersSchema.properties).map( (key) => { @@ -805,10 +892,9 @@ export const ProjectPage = () => { )}
); - } + }, )} - //
)}
{isLlmProviderSelected && @@ -821,17 +907,12 @@ export const ProjectPage = () => { title={param.title} /> ))} - {isLlmProviderSelected &&

Model

} - {isLlmProviderSelected && !modelsLoaded && ( - + + {!modelsLoaded && ( +
)} {modelsLoaded && ( -
+
{ />
)} - {isLlmSelected && modelParametersSchema && ( -
- {Object.keys(modelParametersSchema.properties).map((key) => { - const property = modelParametersSchema.properties[key]; - return ( -
-

- {property.title}{" "} - {modelFormValues[key] !== undefined && - modelFormValues[key] !== "" - ? "(" + modelFormValues[key] + ")" - : ""} -

-

- {property.description} -

- {property.type === "number" && ( - { - setModelFormValue((vals) => ({ - ...vals, - [key]: e.target.value, - })); - }} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error Ok - value={modelFormValues[key]} - /> - )} - {property.type === "integer" && ( - { - setModelFormValue((vals) => ({ - ...vals, - [key]: e.target.value, - })); - }} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error Ok - value={modelFormValues[key]} - /> - )} -
- ); - })} -
- )} + +
+ + +
-
- - - - Create Few-shot - -
From 73e4a4d23e6ff3f2fac10c3be7a8e9853b86d3ca Mon Sep 17 00:00:00 2001 From: Aleksi Huotala <7612995+alehuo@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:06:57 +0200 Subject: [PATCH 11/15] Extract provider config to its own component for easier refactoring in the future --- client/src/pages/ProjectPage.tsx | 199 +++++++++++++++++-------------- 1 file changed, 108 insertions(+), 91 deletions(-) diff --git a/client/src/pages/ProjectPage.tsx b/client/src/pages/ProjectPage.tsx index a708dda..883936c 100644 --- a/client/src/pages/ProjectPage.tsx +++ b/client/src/pages/ProjectPage.tsx @@ -131,6 +131,108 @@ const ModelConfiguration: React.FC = ({ ) : null; }; +type ProviderConfigurationProps = { + modelsLoaded: boolean; + providerParametersSchema?: Provider["provider_parameters_json_schema"]; + providerFormValues: Record; + setProviderFormValue: React.Dispatch< + React.SetStateAction> + >; +}; + +const ProviderConfiguration: React.FC = ({ + modelsLoaded, + providerParametersSchema, + providerFormValues, + setProviderFormValue, +}) => { + return providerParametersSchema ? ( +
+ {Object.keys(providerParametersSchema.properties).map((key) => { + const property = providerParametersSchema.properties[key]; + return ( +
+

+ {property.title}{" "} + {providerFormValues[key] !== undefined && + property.type !== "string" && + providerFormValues[key] !== "" + ? "(" + providerFormValues[key] + ")" + : ""} +

+

{property.description}

+ {property.type === "number" && ( + { + const val = + e.target.value === "" ? "" : parseFloat(e.target.value); + if (!Number.isNaN(val)) { + setProviderFormValue((vals) => ({ + ...vals, + [key]: val, + })); + } + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Ok + value={providerFormValues[key]} + /> + )} + {property.type === "string" && ( + { + setProviderFormValue((vals) => ({ + ...vals, + [key]: e.target.value, + })); + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Ok + value={providerFormValues[key]} + /> + )} + {property.type === "integer" && ( + { + const val = + e.target.value === "" ? "" : parseInt(e.target.value, 10); + if (!Number.isNaN(val)) { + setProviderFormValue((vals) => ({ + ...vals, + [key]: val, + })); + } + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Ok + value={providerFormValues[key]} + /> + )} +
+ ); + })} +
+ ) : null; +}; + const ActionComponent: React.FC = ({ hasPapers, projectUuid, @@ -804,97 +906,12 @@ export const ProjectPage = () => {
)} */} {isLlmProviderSelected && providerParametersSchema && ( - <> - {Object.keys(providerParametersSchema.properties).map( - (key) => { - const property = providerParametersSchema.properties[key]; - return ( -
-

- {property.title}{" "} - {providerFormValues[key] !== undefined && - property.type !== "string" && - providerFormValues[key] !== "" - ? "(" + providerFormValues[key] + ")" - : ""} -

-

- {property.description} -

- {property.type === "number" && ( - { - const val = - e.target.value === "" - ? "" - : parseFloat(e.target.value); - if (!Number.isNaN(val)) { - setProviderFormValue((vals) => ({ - ...vals, - [key]: val, - })); - } - }} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error Ok - value={providerFormValues[key]} - /> - )} - {property.type === "string" && ( - { - setProviderFormValue((vals) => ({ - ...vals, - [key]: e.target.value, - })); - }} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error Ok - value={providerFormValues[key]} - /> - )} - {property.type === "integer" && ( - { - const val = - e.target.value === "" - ? "" - : parseInt(e.target.value, 10); - if (!Number.isNaN(val)) { - setProviderFormValue((vals) => ({ - ...vals, - [key]: val, - })); - } - }} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error Ok - value={providerFormValues[key]} - /> - )} -
- ); - }, - )} - + )}
{isLlmProviderSelected && From 360a4f07ed08e7d070400bbb1d3eab2da31db44f Mon Sep 17 00:00:00 2001 From: Aleksi Huotala <7612995+alehuo@users.noreply.github.com> Date: Tue, 20 Jan 2026 09:26:53 +0200 Subject: [PATCH 12/15] Use Details and Summary native components to hide/show model config --- client/src/pages/ProjectPage.tsx | 204 +++++++++++++++++--------- server/src/core/llm/providers/mock.py | 12 +- 2 files changed, 142 insertions(+), 74 deletions(-) diff --git a/client/src/pages/ProjectPage.tsx b/client/src/pages/ProjectPage.tsx index 883936c..350f58f 100644 --- a/client/src/pages/ProjectPage.tsx +++ b/client/src/pages/ProjectPage.tsx @@ -73,66 +73,110 @@ const ModelConfiguration: React.FC = ({ modelFormValues, setModelFormValue, }) => { + if ( + modelParametersSchema === undefined || + Object.keys(modelParametersSchema.properties).length === 0 + ) { + return null; + } return isLlmSelected && modelParametersSchema ? ( -
- {Object.keys(modelParametersSchema.properties).map((key) => { - const property = modelParametersSchema.properties[key]; - return ( -
-

- {property.title}{" "} - {modelFormValues[key] !== undefined && modelFormValues[key] !== "" - ? "(" + modelFormValues[key] + ")" - : ""} -

-

{property.description}

- {property.type === "number" && ( - { - setModelFormValue((vals) => ({ - ...vals, - [key]: e.target.value, - })); - }} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error Ok - value={modelFormValues[key]} - /> - )} - {property.type === "integer" && ( - { - setModelFormValue((vals) => ({ - ...vals, - [key]: e.target.value, - })); - }} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error Ok - value={modelFormValues[key]} - /> - )} +
+ +
+
Advanced
+
+ {Object.keys(modelParametersSchema.properties).map((key) => { + const property = modelParametersSchema.properties[key]; + return ( + {`${property.title}: ${ + modelFormValues[key] !== undefined && + modelFormValues[key] !== "" && + modelFormValues[key] + }`} + ); + })}
- ); - })} -
+
+ + +
+ {Object.keys(modelParametersSchema.properties).map((key) => { + const property = modelParametersSchema.properties[key]; + return ( +
+
+ + + {modelFormValues[key] !== undefined && + modelFormValues[key] !== "" ? ( + <>{modelFormValues[key]} + ) : ( + "" + )} + +
+ {property.type === "number" && ( + { + setModelFormValue((vals) => ({ + ...vals, + [key]: e.target.value, + })); + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Ok + value={modelFormValues[key]} + /> + )} + {property.type === "integer" && ( + { + setModelFormValue((vals) => ({ + ...vals, + [key]: e.target.value, + })); + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Ok + value={modelFormValues[key]} + /> + )} +

{property.description}

+
+ ); + })} +
+ ) : null; }; type ProviderConfigurationProps = { - modelsLoaded: boolean; + modelSelected: boolean; providerParametersSchema?: Provider["provider_parameters_json_schema"]; providerFormValues: Record; setProviderFormValue: React.Dispatch< @@ -141,13 +185,23 @@ type ProviderConfigurationProps = { }; const ProviderConfiguration: React.FC = ({ - modelsLoaded, + modelSelected, providerParametersSchema, providerFormValues, setProviderFormValue, }) => { + if ( + providerParametersSchema === null || + providerParametersSchema === undefined || + Object.keys(providerParametersSchema.properties).length === 0 + ) { + return null; + } + const cx = classNames( + "rounded-lg p-2 h-8 bg-whitecursor-pointer text-sm border-1 border-slate-400 disabled:cursor-not-allowed disabled:border-gray-200 disabled:text-gray-400 bg-white accent-slate-800", + ); return providerParametersSchema ? ( -
+
{Object.keys(providerParametersSchema.properties).map((key) => { const property = providerParametersSchema.properties[key]; return ( @@ -155,20 +209,25 @@ const ProviderConfiguration: React.FC = ({ className="flex flex-col justify-between gap-1 w-full" key={`property_${key}`} > -

- {property.title}{" "} - {providerFormValues[key] !== undefined && - property.type !== "string" && - providerFormValues[key] !== "" - ? "(" + providerFormValues[key] + ")" - : ""} -

-

{property.description}

+
+ + + {providerFormValues[key] !== undefined && + property.type !== "string" && + providerFormValues[key] !== "" ? ( + <>{providerFormValues[key]} + ) : ( + "" + )} + +
{property.type === "number" && ( = ({ {property.type === "string" && ( { setProviderFormValue((vals) => ({ @@ -208,8 +267,8 @@ const ProviderConfiguration: React.FC = ({ {property.type === "integer" && ( { const val = @@ -226,6 +285,7 @@ const ProviderConfiguration: React.FC = ({ value={providerFormValues[key]} /> )} +

{property.description}

); })} @@ -907,7 +967,7 @@ export const ProjectPage = () => { )} */} {isLlmProviderSelected && providerParametersSchema && ( Date: Tue, 20 Jan 2026 09:58:51 +0200 Subject: [PATCH 13/15] UI improvements --- client/src/pages/ProjectPage.tsx | 204 +++++++++++++++++-------------- 1 file changed, 113 insertions(+), 91 deletions(-) diff --git a/client/src/pages/ProjectPage.tsx b/client/src/pages/ProjectPage.tsx index 350f58f..5e7bb27 100644 --- a/client/src/pages/ProjectPage.tsx +++ b/client/src/pages/ProjectPage.tsx @@ -201,95 +201,117 @@ const ProviderConfiguration: React.FC = ({ "rounded-lg p-2 h-8 bg-whitecursor-pointer text-sm border-1 border-slate-400 disabled:cursor-not-allowed disabled:border-gray-200 disabled:text-gray-400 bg-white accent-slate-800", ); return providerParametersSchema ? ( -
- {Object.keys(providerParametersSchema.properties).map((key) => { - const property = providerParametersSchema.properties[key]; - return ( -
-
- - - {providerFormValues[key] !== undefined && - property.type !== "string" && - providerFormValues[key] !== "" ? ( - <>{providerFormValues[key]} - ) : ( - "" - )} - -
- {property.type === "number" && ( - { - const val = - e.target.value === "" ? "" : parseFloat(e.target.value); - if (!Number.isNaN(val)) { - setProviderFormValue((vals) => ({ - ...vals, - [key]: val, - })); - } - }} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error Ok - value={providerFormValues[key]} - /> - )} - {property.type === "string" && ( - { - setProviderFormValue((vals) => ({ - ...vals, - [key]: e.target.value, - })); - }} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error Ok - value={providerFormValues[key]} - /> - )} - {property.type === "integer" && ( - { - const val = - e.target.value === "" ? "" : parseInt(e.target.value, 10); - if (!Number.isNaN(val)) { +
+ +
+
Advanced
+
+ Provider configuration. +
+
+ +
+
+ {Object.keys(providerParametersSchema.properties).map((key) => { + const property = providerParametersSchema.properties[key]; + return ( +
+
+ + + {providerFormValues[key] !== undefined && + property.type !== "string" && + providerFormValues[key] !== "" ? ( + <>{providerFormValues[key]} + ) : ( + "" + )} + +
+ {property.type === "number" && ( + { + const val = + e.target.value === "" ? "" : parseFloat(e.target.value); + if (!Number.isNaN(val)) { + setProviderFormValue((vals) => ({ + ...vals, + [key]: val, + })); + } + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Ok + value={providerFormValues[key]} + /> + )} + {property.type === "string" && ( + { setProviderFormValue((vals) => ({ ...vals, - [key]: val, + [key]: e.target.value, })); - } - }} - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error Ok - value={providerFormValues[key]} - /> - )} -

{property.description}

-
- ); - })} -
+ }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Ok + value={providerFormValues[key]} + /> + )} + {property.type === "integer" && ( + { + const val = + e.target.value === "" ? "" : parseInt(e.target.value, 10); + if (!Number.isNaN(val)) { + setProviderFormValue((vals) => ({ + ...vals, + [key]: val, + })); + } + }} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error Ok + value={providerFormValues[key]} + /> + )} +

{property.description}

+
+ ); + })} +
+ ) : null; }; @@ -1011,14 +1033,14 @@ export const ProjectPage = () => { modelParametersSchema={modelParametersSchema} setModelFormValue={setModelFormValue} /> -
+