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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 24 additions & 8 deletions apps/desktop/src/components/license.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,42 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";

import { useLicense } from "@/hooks/use-license";

const REFRESH_INTERVAL = 30 * 60 * 1000;
const INITIAL_DELAY = 5000;
const RATE_LIMIT = 60 * 60 * 1000;

export function LicenseRefreshProvider({ children }: { children: React.ReactNode }) {
const { shouldRefresh, refreshLicense, getLicense } = useLicense();
const { getLicenseStatus, refreshLicense, getLicense } = useLicense();
const lastRefreshAttempt = useRef<number>(0);

useEffect(() => {
if (getLicense.isLoading) {
return;
}

const checkAndRefresh = () => {
if (shouldRefresh() && !refreshLicense.isPending) {
const attemptRefresh = () => {
const status = getLicenseStatus();
const now = Date.now();

if (refreshLicense.isPending || now - lastRefreshAttempt.current < RATE_LIMIT) {
return;
}

if (!status.isValid || status.needsRefresh) {
lastRefreshAttempt.current = now;
refreshLicense.mutate();
}
};

checkAndRefresh();
const interval = setInterval(checkAndRefresh, 1000 * 60 * 10); // 10min
const timeout = setTimeout(attemptRefresh, INITIAL_DELAY);
const interval = setInterval(attemptRefresh, REFRESH_INTERVAL);

return () => clearInterval(interval);
}, [shouldRefresh, refreshLicense, getLicense.isLoading, refreshLicense.isPending]);
return () => {
clearTimeout(timeout);
clearInterval(interval);
};
}, [getLicense.isLoading, getLicenseStatus, refreshLicense.isPending]);

return <>{children}</>;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Trans } from "@lingui/react/macro";
import { useQuery } from "@tanstack/react-query";
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
import useDebouncedCallback from "beautiful-react-hooks/useDebouncedCallback";
import { useEffect } from "react";

import {
Expand All @@ -13,9 +16,6 @@ import {
import { Input } from "@hypr/ui/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select";
import { cn } from "@hypr/ui/lib/utils";
import { useQuery } from "@tanstack/react-query";
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
import useDebouncedCallback from "beautiful-react-hooks/useDebouncedCallback";
import { useState } from "react";
import { SharedCustomEndpointProps } from "./shared";

Expand Down Expand Up @@ -48,14 +48,11 @@ const openrouterModels = [

export function LLMCustomView({
customLLMEnabled,
selectedLLMModel,
setSelectedLLMModel,
setCustomLLMEnabledMutation,
configureCustomEndpoint,
openAccordion,
setOpenAccordion,
customLLMConnection,
getCustomLLMModel,
openaiForm,
geminiForm,
openrouterForm,
Expand Down Expand Up @@ -230,12 +227,6 @@ export function LLMCustomView({

return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">
<Trans>Custom Endpoints</Trans>
</h2>
</div>

<div className="max-w-2xl space-y-4">
{/* OpenAI Accordion */}
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Trans } from "@lingui/react/macro";
import { useQuery } from "@tanstack/react-query";
import { openPath } from "@tauri-apps/plugin-opener";
import { DownloadIcon, FolderIcon } from "lucide-react";
import { useEffect } from "react";

import { commands as localLlmCommands, SupportedModel } from "@hypr/plugin-local-llm";
import { commands as localLlmCommands, type SupportedModel } from "@hypr/plugin-local-llm";
import { Button } from "@hypr/ui/components/ui/button";
import { cn } from "@hypr/ui/lib/utils";
import { SharedLLMProps } from "./shared";
import { type LLMModel, SharedLLMProps } from "./shared";

export function LLMLocalView({
customLLMEnabled,
Expand All @@ -16,33 +16,40 @@ export function LLMLocalView({
downloadingModels,
llmModelsState,
handleModelDownload,
handleShowFileLocation,
}: SharedLLMProps) {
// call backend for the current selected LLM model and sets it
const currentLLMModel = useQuery({
queryKey: ["current-llm-model"],
queryFn: () => localLlmCommands.getCurrentModel(),
});

const handleShowFileLocation = async () => {
localLlmCommands.modelsDir().then((path) => openPath(path));
};

useEffect(() => {
if (currentLLMModel.data && !customLLMEnabled.data) {
setSelectedLLMModel(currentLLMModel.data);
}
}, [currentLLMModel.data, customLLMEnabled.data, setSelectedLLMModel]);

const handleLocalModelSelection = (model: LLMModel) => {
if (model.available && model.downloaded) {
setSelectedLLMModel(model.key);
localLlmCommands.setCurrentModel(model.key as SupportedModel);
// CRITICAL: Disable custom LLM when local model is selected
setCustomLLMEnabledMutation.mutate(false);
localLlmCommands.restartServer();
}
};

return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<h2 className="text-lg font-semibold">
<Trans>Local Models</Trans>
</h2>
</div>

<div className="max-w-2xl">
<div className="space-y-2">
{llmModelsState.map((model) => (
<div
key={model.key}
onClick={() => handleLocalModelSelection(model)}
className={cn(
"group relative p-3 rounded-lg border-2 transition-all flex items-center justify-between",
selectedLLMModel === model.key && model.available && model.downloaded && !customLLMEnabled.data
Expand All @@ -51,15 +58,6 @@ export function LLMLocalView({
? "border-dashed border-gray-300 hover:border-gray-400 bg-white cursor-pointer"
: "border-dashed border-gray-200 bg-gray-50 cursor-not-allowed",
)}
onClick={() => {
if (model.available && model.downloaded) {
setSelectedLLMModel(model.key);
localLlmCommands.setCurrentModel(model.key as SupportedModel);
// CRITICAL: Disable custom LLM when local model is selected
setCustomLLMEnabledMutation.mutate(false);
localLlmCommands.restartServer();
}
}}
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-4">
Expand Down Expand Up @@ -96,10 +94,7 @@ export function LLMLocalView({
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
handleShowFileLocation("llm");
}}
onClick={handleShowFileLocation}
className="text-xs h-7 px-2 flex items-center gap-1"
>
<FolderIcon className="w-3 h-3" />
Expand Down Expand Up @@ -132,15 +127,6 @@ export function LLMLocalView({
</Button>
)}
</div>

{!model.available && (
<div className="absolute inset-0 bg-white/90 backdrop-blur-sm rounded-lg flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<div className="text-center">
<div className="text-base font-semibold text-gray-700 mb-1">Coming Soon</div>
<div className="text-sm text-gray-500">Feature in development</div>
</div>
</div>
)}
</div>
))}
</div>
Expand Down
10 changes: 4 additions & 6 deletions apps/desktop/src/components/settings/components/ai/shared.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Connection } from "@hypr/plugin-connector";
import { cn } from "@hypr/ui/lib/utils";
import { type SupportedModel } from "@hypr/plugin-local-llm";
import { UseMutationResult, UseQueryResult } from "@tanstack/react-query";
import { UseFormReturn } from "react-hook-form";

import { cn } from "@hypr/ui/lib/utils";

export const RatingDisplay = (
{ label, rating, maxRating = 3, icon: Icon }: {
label: string;
Expand Down Expand Up @@ -42,7 +44,7 @@ export const LanguageDisplay = ({ support }: { support: "multilingual" | "englis
};

export interface LLMModel {
key: string;
key: SupportedModel;
name: string;
description: string;
available: boolean;
Expand All @@ -53,8 +55,6 @@ export interface LLMModel {
export interface STTModel {
key: string;
name: string;
accuracy: number;
speed: number;
size: string;
downloaded: boolean;
fileName: string;
Expand Down Expand Up @@ -95,7 +95,6 @@ export interface SharedSTTProps {
setSttModels: React.Dispatch<React.SetStateAction<STTModel[]>>;
downloadingModels: Set<string>;
handleModelDownload: (modelKey: string) => Promise<void>;
handleShowFileLocation: (modelType: "stt" | "llm") => Promise<void>;
}

export interface SharedLLMProps {
Expand All @@ -113,7 +112,6 @@ export interface SharedLLMProps {

// Functions
handleModelDownload: (modelKey: string) => Promise<void>;
handleShowFileLocation: (modelType: "stt" | "llm") => Promise<void>;
}

export interface SharedCustomEndpointProps extends SharedLLMProps {
Expand Down
Loading
Loading