feat(28-02): create ollamaApi client and Hermes Ollama model dropdown
- Add ui/src/api/ollama.ts with ollamaApi.status() and ollamaApi.models() - Replace free-text Model input with hybrid dropdown/fallback in HermesLocalConfigFields - Dropdown shows pulled Ollama models with * prefix for recommended entries - Install callout shown when Ollama is absent (with link to installUrl) - Edit mode: selecting an Ollama model atomically sets model + provider:custom + base_url - Manual entry fallback via "Other (manual entry)..." option or when Ollama absent - Uses useCompany() hook for companyId (consistent with AgentConfigForm pattern)
This commit is contained in:
parent
5345b67f92
commit
926b3a8763
2 changed files with 141 additions and 16 deletions
|
|
@ -1,9 +1,13 @@
|
|||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { AdapterConfigFieldsProps } from "../types";
|
||||
import {
|
||||
Field,
|
||||
DraftInput,
|
||||
} from "../../components/agent-config-primitives";
|
||||
import { ChoosePathButton } from "../../components/PathInstructionsModal";
|
||||
import { useCompany } from "../../context/CompanyContext";
|
||||
import { ollamaApi } from "../../api/ollama";
|
||||
|
||||
const inputClass =
|
||||
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
|
||||
|
|
@ -19,6 +23,63 @@ export function HermesLocalConfigFields({
|
|||
mark,
|
||||
hideInstructionsFile,
|
||||
}: AdapterConfigFieldsProps) {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const companyId = selectedCompanyId;
|
||||
|
||||
const [manualEntry, setManualEntry] = useState(false);
|
||||
|
||||
const { data: ollamaStatus } = useQuery({
|
||||
queryKey: ["ollama", "status", companyId],
|
||||
queryFn: () => ollamaApi.status(companyId!),
|
||||
enabled: Boolean(companyId),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const { data: ollamaModels } = useQuery({
|
||||
queryKey: ["ollama", "models", companyId],
|
||||
queryFn: () => ollamaApi.models(companyId!),
|
||||
enabled: Boolean(companyId && ollamaStatus?.installed),
|
||||
staleTime: 60_000,
|
||||
});
|
||||
|
||||
const currentModel = isCreate
|
||||
? (values!.model ?? "")
|
||||
: eff("adapterConfig", "model", String(config.model ?? ""));
|
||||
|
||||
function handleSelectModel(selectedModel: string) {
|
||||
if (selectedModel === "__manual__") {
|
||||
setManualEntry(true);
|
||||
return;
|
||||
}
|
||||
if (selectedModel === "") return;
|
||||
// Set fields when selecting an Ollama model
|
||||
if (isCreate) {
|
||||
// In create mode, CreateConfigValues does not have provider/base_url;
|
||||
// only model is stored via form values. The server resolves provider
|
||||
// at runtime from the model name or ~/.hermes/config.yaml.
|
||||
set!({ model: selectedModel });
|
||||
} else {
|
||||
// In edit mode, set all three fields atomically on adapterConfig
|
||||
mark("adapterConfig", "model", selectedModel);
|
||||
mark("adapterConfig", "provider", "custom");
|
||||
mark("adapterConfig", "base_url", "http://localhost:11434/v1");
|
||||
}
|
||||
}
|
||||
|
||||
function formatModelLabel(m: { name: string; parameterSize: string; quantization: string; recommended: boolean; recommendationReason: string | null }) {
|
||||
const params = m.parameterSize ? ` (${m.parameterSize}, ${m.quantization})` : "";
|
||||
const recSuffix = m.recommended ? " - Recommended for your system" : "";
|
||||
const star = m.recommended ? "* " : "";
|
||||
return `${star}${m.name}${params}${recSuffix}`;
|
||||
}
|
||||
|
||||
const showDropdown =
|
||||
ollamaStatus?.installed === true &&
|
||||
(ollamaModels?.models?.length ?? 0) > 0 &&
|
||||
!manualEntry;
|
||||
|
||||
const showInstallCallout = ollamaStatus?.installed === false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideInstructionsFile && (
|
||||
|
|
@ -47,26 +108,59 @@ export function HermesLocalConfigFields({
|
|||
</div>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label="Model"
|
||||
hint="Provider/model format (e.g. anthropic/claude-sonnet-4). Leave blank to use Hermes default."
|
||||
hint={
|
||||
showDropdown
|
||||
? "Select a locally pulled Ollama model. Recommended models are marked with *."
|
||||
: "Provider/model format (e.g. anthropic/claude-sonnet-4). Leave blank to use Hermes default."
|
||||
}
|
||||
>
|
||||
<DraftInput
|
||||
value={
|
||||
isCreate
|
||||
? values!.model ?? ""
|
||||
: eff("adapterConfig", "model", String(config.model ?? ""))
|
||||
}
|
||||
onCommit={(v) =>
|
||||
isCreate
|
||||
? set!({ model: v })
|
||||
: mark("adapterConfig", "model", v || undefined)
|
||||
}
|
||||
immediate
|
||||
className={inputClass}
|
||||
placeholder="anthropic/claude-sonnet-4"
|
||||
/>
|
||||
{showInstallCallout && (
|
||||
<div className="mb-2 rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm">
|
||||
<span className="text-amber-200">Ollama is not detected. </span>
|
||||
<a
|
||||
href={ollamaStatus.installUrl ?? "https://ollama.com/download"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-amber-400 underline hover:text-amber-300"
|
||||
>
|
||||
Install Ollama
|
||||
</a>
|
||||
<span className="text-muted-foreground"> to use local models.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDropdown ? (
|
||||
<select
|
||||
className={inputClass}
|
||||
value={currentModel || ""}
|
||||
onChange={(e) => handleSelectModel(e.target.value)}
|
||||
>
|
||||
<option value="">Select a model...</option>
|
||||
{(ollamaModels?.models ?? []).map((m) => (
|
||||
<option key={m.name} value={m.name}>
|
||||
{formatModelLabel(m)}
|
||||
</option>
|
||||
))}
|
||||
<option value="__manual__">Other (manual entry)...</option>
|
||||
</select>
|
||||
) : (
|
||||
<DraftInput
|
||||
value={currentModel}
|
||||
onCommit={(v) =>
|
||||
isCreate
|
||||
? set!({ model: v })
|
||||
: mark("adapterConfig", "model", v || undefined)
|
||||
}
|
||||
immediate
|
||||
className={inputClass}
|
||||
placeholder="anthropic/claude-sonnet-4"
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{!isCreate && (
|
||||
<>
|
||||
<Field
|
||||
|
|
|
|||
31
ui/src/api/ollama.ts
Normal file
31
ui/src/api/ollama.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { api } from "./client";
|
||||
|
||||
export interface OllamaStatus {
|
||||
installed: boolean;
|
||||
version: string | null;
|
||||
installUrl: string;
|
||||
}
|
||||
|
||||
export interface OllamaModel {
|
||||
name: string;
|
||||
parameterSize: string;
|
||||
quantization: string;
|
||||
sizeBytes: number;
|
||||
family: string;
|
||||
recommended: boolean;
|
||||
recommendationReason: string | null;
|
||||
}
|
||||
|
||||
export interface OllamaModelsResponse {
|
||||
models: OllamaModel[];
|
||||
ramGb: number;
|
||||
}
|
||||
|
||||
export const ollamaApi = {
|
||||
status(companyId: string): Promise<OllamaStatus> {
|
||||
return api.get<OllamaStatus>(`/companies/${companyId}/ollama/status`);
|
||||
},
|
||||
models(companyId: string): Promise<OllamaModelsResponse> {
|
||||
return api.get<OllamaModelsResponse>(`/companies/${companyId}/ollama/models`);
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue