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 type { AdapterConfigFieldsProps } from "../types";
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
DraftInput,
|
DraftInput,
|
||||||
} from "../../components/agent-config-primitives";
|
} from "../../components/agent-config-primitives";
|
||||||
import { ChoosePathButton } from "../../components/PathInstructionsModal";
|
import { ChoosePathButton } from "../../components/PathInstructionsModal";
|
||||||
|
import { useCompany } from "../../context/CompanyContext";
|
||||||
|
import { ollamaApi } from "../../api/ollama";
|
||||||
|
|
||||||
const inputClass =
|
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";
|
"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,
|
mark,
|
||||||
hideInstructionsFile,
|
hideInstructionsFile,
|
||||||
}: AdapterConfigFieldsProps) {
|
}: 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{!hideInstructionsFile && (
|
{!hideInstructionsFile && (
|
||||||
|
|
@ -47,26 +108,59 @@ export function HermesLocalConfigFields({
|
||||||
</div>
|
</div>
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label="Model"
|
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
|
{showInstallCallout && (
|
||||||
value={
|
<div className="mb-2 rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm">
|
||||||
isCreate
|
<span className="text-amber-200">Ollama is not detected. </span>
|
||||||
? values!.model ?? ""
|
<a
|
||||||
: eff("adapterConfig", "model", String(config.model ?? ""))
|
href={ollamaStatus.installUrl ?? "https://ollama.com/download"}
|
||||||
}
|
target="_blank"
|
||||||
onCommit={(v) =>
|
rel="noopener noreferrer"
|
||||||
isCreate
|
className="text-amber-400 underline hover:text-amber-300"
|
||||||
? set!({ model: v })
|
>
|
||||||
: mark("adapterConfig", "model", v || undefined)
|
Install Ollama
|
||||||
}
|
</a>
|
||||||
immediate
|
<span className="text-muted-foreground"> to use local models.</span>
|
||||||
className={inputClass}
|
</div>
|
||||||
placeholder="anthropic/claude-sonnet-4"
|
)}
|
||||||
/>
|
|
||||||
|
{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>
|
</Field>
|
||||||
|
|
||||||
{!isCreate && (
|
{!isCreate && (
|
||||||
<>
|
<>
|
||||||
<Field
|
<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