Skip to content
Closed
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions apps/desktop/src/main/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ import {
testLMStudioConnection,
fetchLMStudioModels,
validateLMStudioConfig,
testHuggingFaceLocalConnection,
fetchHuggingFaceLocalModels,
validateHuggingFaceLocalConfig,
} from '@accomplish_ai/agent-core';
import { getStorage } from '../store/storage';
import { getOpenAiOauthStatus } from '@accomplish_ai/agent-core';
Expand Down Expand Up @@ -81,6 +84,7 @@ import type {
AzureFoundryConfig,
LiteLLMConfig,
LMStudioConfig,
HuggingFaceLocalConfig,
} from '@accomplish_ai/agent-core';
import {
DEFAULT_PROVIDERS,
Expand Down Expand Up @@ -870,6 +874,35 @@ export function registerIPCHandlers(): void {
},
);

// ── HuggingFace Local handlers ─────────────────────────────────────────

handle('huggingface-local:test-connection', async (_event: IpcMainInvokeEvent, url: string) => {
return testHuggingFaceLocalConnection(url);
});

handle('huggingface-local:fetch-models', async (_event: IpcMainInvokeEvent) => {
const config = storage.getHuggingFaceLocalConfig();
if (!config || !config.serverUrl) {
return { success: false, error: 'No HuggingFace Local server configured' };
}

return fetchHuggingFaceLocalModels({ baseUrl: config.serverUrl });
});

handle('huggingface-local:get-config', async (_event: IpcMainInvokeEvent) => {
return storage.getHuggingFaceLocalConfig();
});

handle(
'huggingface-local:set-config',
async (_event: IpcMainInvokeEvent, config: HuggingFaceLocalConfig | null) => {
if (config !== null) {
validateHuggingFaceLocalConfig(config);
}
storage.setHuggingFaceLocalConfig(config);
},
);

handle(
'provider:fetch-models',
async (
Expand Down
51 changes: 51 additions & 0 deletions apps/desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,57 @@ const accomplishAPI = {
} | null,
): Promise<void> => ipcRenderer.invoke('lmstudio:set-config', config),

// HuggingFace Local configuration
testHuggingFaceLocalConnection: (
url: string,
): Promise<{
success: boolean;
models?: Array<{
id: string;
displayName: string;
size: number;
toolSupport?: 'supported' | 'unsupported' | 'unknown';
}>;
error?: string;
}> => ipcRenderer.invoke('huggingface-local:test-connection', url),

fetchHuggingFaceLocalModels: (): Promise<{
success: boolean;
models?: Array<{
id: string;
displayName: string;
size: number;
toolSupport?: 'supported' | 'unsupported' | 'unknown';
}>;
error?: string;
}> => ipcRenderer.invoke('huggingface-local:fetch-models'),

getHuggingFaceLocalConfig: (): Promise<{
serverUrl: string;
enabled: boolean;
lastValidated?: number;
models?: Array<{
id: string;
displayName: string;
size: number;
toolSupport?: 'supported' | 'unsupported' | 'unknown';
}>;
} | null> => ipcRenderer.invoke('huggingface-local:get-config'),

setHuggingFaceLocalConfig: (
config: {
serverUrl: string;
enabled: boolean;
lastValidated?: number;
models?: Array<{
id: string;
displayName: string;
size: number;
toolSupport?: 'supported' | 'unsupported' | 'unknown';
}>;
} | null,
): Promise<void> => ipcRenderer.invoke('huggingface-local:set-config', config),

// Bedrock
validateBedrockCredentials: (credentials: string) =>
ipcRenderer.invoke('bedrock:validate', credentials),
Expand Down
14 changes: 12 additions & 2 deletions apps/web/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"moonshot": "Moonshot",
"litellm": "LiteLLM",
"vertex": "Vertex AI",
"azure-foundry": "Azure AI Foundry"
"azure-foundry": "Azure AI Foundry",
"huggingface-local": "HuggingFace Local"
},
"providerLabels": {
"anthropic": "Claude models",
Expand All @@ -71,7 +72,8 @@
"moonshot": "Kimi models",
"litellm": "Unified proxy",
"vertex": "Google Cloud",
"azure-foundry": "Azure Service"
"azure-foundry": "Azure Service",
"huggingface-local": "Local inference"
},
"status": {
"connected": "Connected",
Expand Down Expand Up @@ -284,6 +286,14 @@
"description": "Access 200+ AI models through OpenRouter's unified API.",
"fetchModelsFailed": "Failed to fetch models"
},
"huggingfaceLocal": {
"serverUrl": "HuggingFace Local Server URL",
"serverHint": "Run a local HuggingFace Transformers.js inference server with ONNX Runtime",
"selectModel": "Please select a model",
"selectModelPlaceholder": "Select a model...",
"infoBannerTitle": "Local inference with Transformers.js",
"infoBannerDescription": "Models run locally using ONNX Runtime. Smaller quantized models (Q4) are recommended for faster inference."
},
"litellm": {
"description": "Connect to your LiteLLM proxy server for unified model access.",
"serverUrl": "Server URL",
Expand Down
Binary file added apps/web/public/assets/ai-logos/huggingface.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/src/client/components/settings/ProviderGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const PROVIDER_ORDER: ProviderId[] = [
'openrouter',
'litellm',
'minimax',
'huggingface-local',
];

interface ProviderGridProps {
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/client/components/settings/ProviderSettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
OpenRouterProviderForm,
LiteLLMProviderForm,
LMStudioProviderForm,
HuggingFaceLocalProviderForm,
VertexProviderForm,
} from './providers';
import { ZaiProviderForm } from './providers/ZaiProviderForm';
Expand Down Expand Up @@ -112,6 +113,17 @@ export function ProviderSettingsPanel({
/>
);
}
if (providerId === 'huggingface-local') {
return (
<HuggingFaceLocalProviderForm
connectedProvider={connectedProvider}
onConnect={onConnect}
onDisconnect={onDisconnect}
onModelChange={onModelChange}
showModelError={showModelError}
/>
);
}
// Default to Ollama for other local providers
return (
<OllamaProviderForm
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AnimatePresence, motion } from 'framer-motion';
import { getAccomplish } from '@/lib/accomplish';
import { settingsVariants, settingsTransitions } from '@/lib/animations';
import type {
ConnectedProvider,
HuggingFaceLocalCredentials,
ToolSupportStatus,
} from '@accomplish_ai/agent-core/common';
import { HF_LOCAL_DEFAULT_URL, type HuggingFaceLocalModel } from '@accomplish_ai/agent-core';
import {
ConnectButton,
ConnectedControls,
ProviderFormHeader,
FormError,
ModelSelector,
} from '../shared';

import huggingfaceLogo from '/assets/ai-logos/huggingface.png';

interface HuggingFaceLocalProviderFormProps {
connectedProvider?: ConnectedProvider;
onConnect: (provider: ConnectedProvider) => void;
onDisconnect: () => void;
onModelChange: (modelId: string) => void;
showModelError: boolean;
}

export function HuggingFaceLocalProviderForm({
connectedProvider,
onConnect,
onDisconnect,
onModelChange,
showModelError,
}: HuggingFaceLocalProviderFormProps) {
const { t } = useTranslation('settings');
const [serverUrl, setServerUrl] = useState(HF_LOCAL_DEFAULT_URL);
const [connecting, setConnecting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [availableModels, setAvailableModels] = useState<HuggingFaceLocalModel[]>([]);

const isConnected = connectedProvider?.connectionStatus === 'connected';

const handleConnect = async () => {
setConnecting(true);
setError(null);

try {
const accomplish = getAccomplish();
const result = await accomplish.testHuggingFaceLocalConnection(serverUrl);

if (!result.success) {
setError(result.error || 'Connection failed');
setConnecting(false);
return;
}

const models = (result.models || []) as HuggingFaceLocalModel[];
setAvailableModels(models);

const provider: ConnectedProvider = {
providerId: 'huggingface-local',
connectionStatus: 'connected',
selectedModelId: null,
credentials: {
type: 'huggingface-local',
serverUrl,
} as HuggingFaceLocalCredentials,
lastConnectedAt: new Date().toISOString(),
availableModels: models.map((m) => ({
id: `huggingface-local/${m.id}`,
name: m.displayName,
toolSupport: m.toolSupport || 'unknown',
})),
};

onConnect(provider);
} catch (err) {
setError(err instanceof Error ? err.message : 'Connection failed');
} finally {
setConnecting(false);
}
};

const models: HuggingFaceLocalModel[] = (
connectedProvider?.availableModels || availableModels
).map((m) => {
const id = m.id.replace(/^huggingface-local\//, '');
return {
id,
displayName:
('name' in m ? m.name : undefined) || ('displayName' in m ? (m.displayName as string) : id),
size: 0,
toolSupport:
('toolSupport' in m ? (m.toolSupport as ToolSupportStatus) : undefined) || 'unknown',
};
});

const selectorModels = models.map((model) => ({
id: `huggingface-local/${model.id}`,
name: model.displayName,
}));

return (
<div
className="rounded-xl border border-border bg-card p-5"
data-testid="provider-settings-panel"
>
<ProviderFormHeader
logoSrc={huggingfaceLogo}
providerName={t('providers.huggingface-local')}
/>

<div className="space-y-3">
<AnimatePresence mode="wait">
{!isConnected ? (
<motion.div
key="disconnected"
variants={settingsVariants.fadeSlide}
initial="initial"
animate="animate"
exit="exit"
transition={settingsTransitions.enter}
className="space-y-3"
>
<div>
<label className="mb-2 block text-sm font-medium text-foreground">
{t('huggingfaceLocal.serverUrl')}
</label>
<input
type="text"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
placeholder={HF_LOCAL_DEFAULT_URL}
data-testid="huggingface-local-server-url"
className="w-full rounded-md border border-input bg-background px-3 py-2.5 text-sm"
/>
<p className="mt-1 text-xs text-muted-foreground">
{t('huggingfaceLocal.serverHint')}
</p>
</div>

<FormError error={error} />
<ConnectButton onClick={handleConnect} connecting={connecting} />
</motion.div>
) : (
<motion.div
key="connected"
variants={settingsVariants.fadeSlide}
initial="initial"
animate="animate"
exit="exit"
transition={settingsTransitions.enter}
className="space-y-3"
>
<div>
<label className="mb-2 block text-sm font-medium text-foreground">
{t('huggingfaceLocal.serverUrl')}
</label>
<input
type="text"
value={
(connectedProvider?.credentials as HuggingFaceLocalCredentials)?.serverUrl ||
HF_LOCAL_DEFAULT_URL
}
disabled
className="w-full rounded-md border border-input bg-muted/50 px-3 py-2.5 text-sm text-muted-foreground"
/>
</div>

<ConnectedControls onDisconnect={onDisconnect} />

<ModelSelector
models={selectorModels}
value={connectedProvider?.selectedModelId || null}
onChange={onModelChange}
error={showModelError && !connectedProvider?.selectedModelId}
errorMessage={t('huggingfaceLocal.selectModel')}
placeholder={t('huggingfaceLocal.selectModelPlaceholder')}
/>

<div className="flex items-start gap-2 rounded-md border border-blue-500/30 bg-blue-500/10 p-3 text-sm text-blue-400">
<svg
className="h-5 w-5 flex-shrink-0 mt-0.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div>
<p className="font-medium">{t('huggingfaceLocal.infoBannerTitle')}</p>
<p className="text-blue-400/80 mt-1">
{t('huggingfaceLocal.infoBannerDescription')}
</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions apps/web/src/client/components/settings/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export { OllamaProviderForm } from './OllamaProviderForm';
export { OpenRouterProviderForm } from './OpenRouterProviderForm';
export { LiteLLMProviderForm } from './LiteLLMProviderForm';
export { LMStudioProviderForm } from './LMStudioProviderForm';
export { HuggingFaceLocalProviderForm } from './HuggingFaceLocalProviderForm';
export { VertexProviderForm } from './vertex';
Loading