feat(nextjs, fastapi): support for ollama custom url
This commit is contained in:
parent
c3add3850e
commit
2542ad6f20
19 changed files with 534 additions and 242 deletions
|
|
@ -15,10 +15,10 @@ can_change_keys = os.getenv("CAN_CHANGE_KEYS") != "false"
|
|||
|
||||
# Ollama model download
|
||||
if not can_change_keys and is_ollama_selected():
|
||||
ollama_model = os.getenv("OLLAMA_MODEL")
|
||||
ollama_model = os.getenv("MODEL")
|
||||
pexels_api_key = os.getenv("PEXELS_API_KEY")
|
||||
if not (ollama_model or pexels_api_key):
|
||||
raise Exception("OLLAMA_MODEL and PEXELS_API_KEY must be provided")
|
||||
raise Exception("MODEL and PEXELS_API_KEY must be provided")
|
||||
|
||||
if ollama_model not in SUPPORTED_OLLAMA_MODELS:
|
||||
raise Exception(f"Model {ollama_model} is not supported")
|
||||
|
|
|
|||
|
|
@ -63,7 +63,9 @@ class UserConfig(BaseModel):
|
|||
LLM: Optional[str] = None
|
||||
OPENAI_API_KEY: Optional[str] = None
|
||||
GOOGLE_API_KEY: Optional[str] = None
|
||||
OLLAMA_MODEL: Optional[str] = None
|
||||
MODEL: Optional[str] = None
|
||||
LLM_PROVIDER_URL: Optional[str] = None
|
||||
LLM_API_KEY: Optional[str] = None
|
||||
PEXELS_API_KEY: Optional[str] = None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class PresentationEditHandler:
|
|||
new_slide_type = new_slide_type.slide_type
|
||||
|
||||
if is_ollama_selected():
|
||||
model = SUPPORTED_OLLAMA_MODELS[os.getenv("OLLAMA_MODEL")]
|
||||
model = SUPPORTED_OLLAMA_MODELS[os.getenv("MODEL")]
|
||||
if not model.supports_graph:
|
||||
if new_slide_type == 5:
|
||||
new_slide_type = 1
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class PresentationGenerateDataHandler:
|
|||
)
|
||||
)
|
||||
supports_graph = True
|
||||
model = SUPPORTED_OLLAMA_MODELS[os.getenv("OLLAMA_MODEL")]
|
||||
model = SUPPORTED_OLLAMA_MODELS[os.getenv("MODEL")]
|
||||
supports_graph = model.supports_graph
|
||||
|
||||
for each in presentation_structure.slides:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import ollama
|
||||
import os
|
||||
import aiohttp
|
||||
from api.models import LogMetadata
|
||||
from api.routers.presentation.models import OllamaModelStatusResponse
|
||||
from api.services.logging import LoggingService
|
||||
from api.utils.model_utils import get_llm_api_key_or, get_llm_provider_url_or
|
||||
|
||||
|
||||
class ListPulledOllamaModelsHandler:
|
||||
|
|
@ -12,20 +14,25 @@ class ListPulledOllamaModelsHandler:
|
|||
extra=log_metadata.model_dump(),
|
||||
)
|
||||
|
||||
response = ollama.list()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
f"{get_llm_provider_url_or()}/api/tags",
|
||||
headers={"Authorization": f"Bearer {get_llm_api_key_or()}"},
|
||||
) as response:
|
||||
response_data = await response.json()
|
||||
|
||||
logging_service.logger.info(
|
||||
logging_service.message(response.model_dump(mode="json")),
|
||||
logging_service.message(response_data),
|
||||
extra=log_metadata.model_dump(),
|
||||
)
|
||||
|
||||
return [
|
||||
OllamaModelStatusResponse(
|
||||
name=model.model,
|
||||
size=model.size,
|
||||
name=model["model"],
|
||||
size=model["size"],
|
||||
status="pulled",
|
||||
downloaded=model.size,
|
||||
downloaded=model["size"],
|
||||
done=True,
|
||||
)
|
||||
for model in response.models
|
||||
for model in response_data["models"]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import asyncio
|
||||
import json
|
||||
import aiohttp
|
||||
from fastapi import BackgroundTasks, HTTPException
|
||||
from api.models import LogMetadata
|
||||
from api.routers.presentation.handlers.list_supported_ollama_models import (
|
||||
|
|
@ -8,7 +8,7 @@ from api.routers.presentation.handlers.list_supported_ollama_models import (
|
|||
from api.routers.presentation.models import OllamaModelStatusResponse
|
||||
from api.services.instances import REDIS_SERVICE
|
||||
from api.services.logging import LoggingService
|
||||
import ollama
|
||||
from api.utils.model_utils import get_llm_api_key_or, get_llm_provider_url_or
|
||||
|
||||
|
||||
class PullOllamaModelHandler:
|
||||
|
|
@ -33,19 +33,34 @@ class PullOllamaModelHandler:
|
|||
detail=f"Model {self.name} is not supported",
|
||||
)
|
||||
|
||||
pulled_models = ollama.list().models
|
||||
filtered_models = list(
|
||||
filter(lambda model: model.model == self.name, pulled_models)
|
||||
)
|
||||
# Check if model is already pulled using LLM_PROVIDER_URL/api/tags
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
f"{get_llm_provider_url_or()}/api/tags",
|
||||
headers={"Authorization": f"Bearer {get_llm_api_key_or()}"},
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
pulled_models = await response.json()
|
||||
filtered_models = [
|
||||
model
|
||||
for model in pulled_models["models"]
|
||||
if model["model"] == self.name
|
||||
]
|
||||
|
||||
# If the model is already pulled, return the model
|
||||
if filtered_models:
|
||||
return OllamaModelStatusResponse(
|
||||
name=self.name,
|
||||
size=filtered_models[0].size,
|
||||
status="pulled",
|
||||
downloaded=filtered_models[0].size,
|
||||
done=True,
|
||||
# If the model is already pulled, return the model
|
||||
if filtered_models:
|
||||
return OllamaModelStatusResponse(
|
||||
name=self.name,
|
||||
size=filtered_models[0]["size"],
|
||||
status="pulled",
|
||||
downloaded=filtered_models[0]["size"],
|
||||
done=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logging_service.logger.warning(
|
||||
f"Failed to check pulled models: {e}",
|
||||
extra=log_metadata.model_dump(),
|
||||
)
|
||||
|
||||
saved_model_status = REDIS_SERVICE.get(f"ollama_models/{self.name}")
|
||||
|
|
@ -64,33 +79,64 @@ class PullOllamaModelHandler:
|
|||
)
|
||||
|
||||
async def pull_model_in_background(self):
|
||||
await asyncio.to_thread(self.pull_model)
|
||||
await self.pull_model()
|
||||
|
||||
def pull_model(self):
|
||||
async def pull_model(self):
|
||||
saved_model_status = OllamaModelStatusResponse(
|
||||
name=self.name,
|
||||
status="pulling",
|
||||
done=False,
|
||||
)
|
||||
log_event_count = 0
|
||||
for event in ollama.pull(self.name, stream=True):
|
||||
log_event_count += 1
|
||||
if log_event_count != 1 and log_event_count % 20 != 0:
|
||||
continue
|
||||
|
||||
if event.completed:
|
||||
saved_model_status.downloaded = event.completed
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
f"{get_llm_provider_url_or()}/api/pull",
|
||||
json={"model": self.name},
|
||||
headers={"Authorization": f"Bearer {get_llm_api_key_or()}"},
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
raise HTTPException(
|
||||
status_code=response.status,
|
||||
detail=f"Failed to pull model: {await response.text()}",
|
||||
)
|
||||
|
||||
if not saved_model_status.size and event.total:
|
||||
saved_model_status.size = event.total
|
||||
async for line in response.content:
|
||||
if not line.strip():
|
||||
continue
|
||||
|
||||
if event.status:
|
||||
saved_model_status.status = event.status
|
||||
try:
|
||||
event = json.loads(line.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
log_event_count += 1
|
||||
if log_event_count != 1 and log_event_count % 20 != 0:
|
||||
continue
|
||||
|
||||
if "completed" in event:
|
||||
saved_model_status.downloaded = event["completed"]
|
||||
|
||||
if not saved_model_status.size and "total" in event:
|
||||
saved_model_status.size = event["total"]
|
||||
|
||||
if "status" in event:
|
||||
saved_model_status.status = event["status"]
|
||||
|
||||
REDIS_SERVICE.set(
|
||||
f"ollama_models/{self.name}",
|
||||
json.dumps(saved_model_status.model_dump(mode="json")),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
saved_model_status.status = "error"
|
||||
saved_model_status.done = True
|
||||
REDIS_SERVICE.set(
|
||||
f"ollama_models/{self.name}",
|
||||
json.dumps(saved_model_status.model_dump(mode="json")),
|
||||
)
|
||||
raise e
|
||||
|
||||
saved_model_status.done = True
|
||||
saved_model_status.status = "pulled"
|
||||
|
|
|
|||
|
|
@ -82,6 +82,9 @@ from api.routers.presentation.models import (
|
|||
)
|
||||
from api.sql_models import PresentationSqlModel
|
||||
from api.utils.utils import handle_errors
|
||||
from image_processor.images_finder import (
|
||||
generate_image_google,
|
||||
)
|
||||
from ppt_generator.models.slide_model import SlideModel
|
||||
|
||||
route_prefix = "/api/v1/ppt"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ def is_ollama_selected() -> bool:
|
|||
return get_selected_llm_provider() == SelectedLLMProvider.OLLAMA
|
||||
|
||||
|
||||
def get_llm_provider_url_or():
|
||||
return os.getenv("LLM_PROVIDER_URL") or "http://localhost:11434"
|
||||
|
||||
|
||||
def get_llm_api_key_or():
|
||||
return os.getenv("LLM_API_KEY") or "ollama"
|
||||
|
||||
|
||||
def get_selected_llm_provider() -> SelectedLLMProvider:
|
||||
return SelectedLLMProvider(os.getenv("LLM"))
|
||||
|
||||
|
|
@ -21,11 +29,9 @@ def get_model_base_url():
|
|||
elif selected_llm == SelectedLLMProvider.GOOGLE:
|
||||
return "https://generativelanguage.googleapis.com/v1beta/openai"
|
||||
elif selected_llm == SelectedLLMProvider.OLLAMA:
|
||||
return os.getenv("LLM_PROVIDER_URL", "http://localhost:11434/v1")
|
||||
elif selected_llm == SelectedLLMProvider.CUSTOM:
|
||||
return os.getenv("LLM_PROVIDER_URL")
|
||||
return os.path.join(get_llm_provider_url_or(), "v1")
|
||||
else:
|
||||
raise ValueError(f"Invalid LLM provider: {selected_llm}")
|
||||
raise ValueError(f"Invalid LLM provider")
|
||||
|
||||
|
||||
def get_llm_api_key():
|
||||
|
|
@ -35,11 +41,9 @@ def get_llm_api_key():
|
|||
elif selected_llm == SelectedLLMProvider.GOOGLE:
|
||||
return os.getenv("GOOGLE_API_KEY")
|
||||
elif selected_llm == SelectedLLMProvider.OLLAMA:
|
||||
return os.getenv("LLM_API_KEY", "ollama")
|
||||
elif selected_llm == SelectedLLMProvider.CUSTOM:
|
||||
return os.getenv("LLM_API_KEY")
|
||||
return get_llm_api_key_or()
|
||||
else:
|
||||
raise ValueError(f"Invalid LLM provider: {selected_llm}")
|
||||
raise ValueError(f"Invalid LLM API key")
|
||||
|
||||
|
||||
def get_llm_client():
|
||||
|
|
@ -57,7 +61,7 @@ def get_large_model():
|
|||
elif selected_llm == SelectedLLMProvider.GOOGLE:
|
||||
return "gemini-2.0-flash"
|
||||
else:
|
||||
return os.getenv("OLLAMA_MODEL")
|
||||
return os.getenv("MODEL")
|
||||
|
||||
|
||||
def get_small_model():
|
||||
|
|
@ -67,7 +71,7 @@ def get_small_model():
|
|||
elif selected_llm == SelectedLLMProvider.GOOGLE:
|
||||
return "gemini-2.0-flash"
|
||||
else:
|
||||
return os.getenv("OLLAMA_MODEL")
|
||||
return os.getenv("MODEL")
|
||||
|
||||
|
||||
def get_nano_model():
|
||||
|
|
@ -77,4 +81,4 @@ def get_nano_model():
|
|||
elif selected_llm == SelectedLLMProvider.GOOGLE:
|
||||
return "gemini-2.0-flash"
|
||||
else:
|
||||
return os.getenv("OLLAMA_MODEL")
|
||||
return os.getenv("MODEL")
|
||||
|
|
|
|||
|
|
@ -44,8 +44,11 @@ def get_user_config():
|
|||
LLM=existing_config.LLM or os.getenv("LLM"),
|
||||
OPENAI_API_KEY=existing_config.OPENAI_API_KEY or os.getenv("OPENAI_API_KEY"),
|
||||
GOOGLE_API_KEY=existing_config.GOOGLE_API_KEY or os.getenv("GOOGLE_API_KEY"),
|
||||
OLLAMA_MODEL=existing_config.OLLAMA_MODEL or os.getenv("OLLAMA_MODEL"),
|
||||
MODEL=existing_config.MODEL or os.getenv("MODEL"),
|
||||
PEXELS_API_KEY=existing_config.PEXELS_API_KEY or os.getenv("PEXELS_API_KEY"),
|
||||
LLM_PROVIDER_URL=existing_config.LLM_PROVIDER_URL
|
||||
or os.getenv("LLM_PROVIDER_URL"),
|
||||
LLM_API_KEY=existing_config.LLM_API_KEY or os.getenv("LLM_API_KEY"),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -57,10 +60,14 @@ def update_env_with_user_config():
|
|||
os.environ["OPENAI_API_KEY"] = user_config.OPENAI_API_KEY
|
||||
if user_config.GOOGLE_API_KEY:
|
||||
os.environ["GOOGLE_API_KEY"] = user_config.GOOGLE_API_KEY
|
||||
if user_config.OLLAMA_MODEL:
|
||||
os.environ["OLLAMA_MODEL"] = user_config.OLLAMA_MODEL
|
||||
if user_config.MODEL:
|
||||
os.environ["MODEL"] = user_config.MODEL
|
||||
if user_config.PEXELS_API_KEY:
|
||||
os.environ["PEXELS_API_KEY"] = user_config.PEXELS_API_KEY
|
||||
if user_config.LLM_PROVIDER_URL:
|
||||
os.environ["LLM_PROVIDER_URL"] = user_config.LLM_PROVIDER_URL
|
||||
if user_config.LLM_API_KEY:
|
||||
os.environ["LLM_API_KEY"] = user_config.LLM_API_KEY
|
||||
|
||||
|
||||
def get_resource(relative_path):
|
||||
|
|
@ -129,35 +136,35 @@ async def download_files(urls: List[str], save_paths: List[str]):
|
|||
async def handle_errors(
|
||||
func, logging_service: LoggingService, log_metadata: LogMetadata, **kwargs
|
||||
):
|
||||
# try:
|
||||
logging_service.logger.info(f"START", extra=log_metadata.model_dump())
|
||||
response = await func(
|
||||
logging_service=logging_service, log_metadata=log_metadata, **kwargs
|
||||
)
|
||||
is_stream = isinstance(response, StreamingResponse)
|
||||
logging_service.logger.info(
|
||||
"STREAMING" if is_stream else "END", extra=log_metadata.model_dump()
|
||||
)
|
||||
return response
|
||||
try:
|
||||
logging_service.logger.info(f"START", extra=log_metadata.model_dump())
|
||||
response = await func(
|
||||
logging_service=logging_service, log_metadata=log_metadata, **kwargs
|
||||
)
|
||||
is_stream = isinstance(response, StreamingResponse)
|
||||
logging_service.logger.info(
|
||||
"STREAMING" if is_stream else "END", extra=log_metadata.model_dump()
|
||||
)
|
||||
return response
|
||||
|
||||
# except HTTPException as e:
|
||||
# log_metadata.status_code = e.status_code
|
||||
# logging_service.logger.error(
|
||||
# f"Raised HTTPException - {e.detail}", extra=log_metadata.model_dump()
|
||||
# )
|
||||
# raise e
|
||||
# except Exception as e:
|
||||
# print(traceback.print_stack())
|
||||
# print(traceback.print_exc())
|
||||
except HTTPException as e:
|
||||
log_metadata.status_code = e.status_code
|
||||
logging_service.logger.error(
|
||||
f"Raised HTTPException - {e.detail}", extra=log_metadata.model_dump()
|
||||
)
|
||||
raise e
|
||||
except Exception as e:
|
||||
print(traceback.print_stack())
|
||||
print(traceback.print_exc())
|
||||
|
||||
# log_metadata.status_code = 400
|
||||
# logging_service.logger.critical(
|
||||
# "Unhandled Exception",
|
||||
# exc_info=True,
|
||||
# stack_info=True,
|
||||
# extra=log_metadata.model_dump(),
|
||||
# )
|
||||
# raise HTTPException(400, "Something went wrong while processing your request.")
|
||||
log_metadata.status_code = 400
|
||||
logging_service.logger.critical(
|
||||
"Unhandled Exception",
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
extra=log_metadata.model_dump(),
|
||||
)
|
||||
raise HTTPException(400, "Something went wrong while processing your request.")
|
||||
|
||||
|
||||
def sanitize_filename(filename: str) -> str:
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import base64
|
|||
import os
|
||||
import uuid
|
||||
import aiohttp
|
||||
from openai import OpenAI
|
||||
from google import genai
|
||||
from google.genai.types import GenerateContentConfig
|
||||
|
||||
from ppt_generator.models.query_and_prompt_models import (
|
||||
ImagePromptWithThemeAndAspectRatio,
|
||||
)
|
||||
from api.utils.utils import download_file, get_resource
|
||||
from api.utils.model_utils import is_ollama_selected
|
||||
from api.utils.model_utils import get_llm_client, is_ollama_selected
|
||||
|
||||
|
||||
async def generate_image(
|
||||
|
|
@ -46,7 +47,7 @@ async def generate_image(
|
|||
|
||||
|
||||
async def generate_image_openai(prompt: str, output_directory: str) -> str:
|
||||
client = OpenAI()
|
||||
client = get_llm_client()
|
||||
result = await asyncio.to_thread(
|
||||
client.images.generate,
|
||||
model="dall-e-3",
|
||||
|
|
@ -66,23 +67,22 @@ async def generate_image_openai(prompt: str, output_directory: str) -> str:
|
|||
|
||||
|
||||
async def generate_image_google(prompt: str, output_directory: str) -> str:
|
||||
# response = await ChatGoogleGenerativeAI(
|
||||
# model="gemini-2.0-flash-preview-image-generation"
|
||||
# ).ainvoke([prompt], generation_config={"response_modalities": ["TEXT", "IMAGE"]})
|
||||
client = genai.Client()
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.0-flash-preview-image-generation",
|
||||
contents=[prompt],
|
||||
config=GenerateContentConfig(response_modalities=["TEXT", "IMAGE"]),
|
||||
)
|
||||
|
||||
# image_block = next(
|
||||
# block
|
||||
# for block in response.content
|
||||
# if isinstance(block, dict) and block.get("image_url")
|
||||
# )
|
||||
for part in response.candidates[0].content.parts:
|
||||
if part.text is not None:
|
||||
print(part.text)
|
||||
elif part.inline_data is not None:
|
||||
image_path = os.path.join(output_directory, f"{str(uuid.uuid4())}.jpg")
|
||||
with open(image_path, "wb") as f:
|
||||
f.write(part.inline_data.data)
|
||||
|
||||
# base64_image = image_block["image_url"].get("url").split(",")[-1]
|
||||
# image_path = os.path.join(output_directory, f"{str(uuid.uuid4())}.jpg")
|
||||
# with open(image_path, "wb") as f:
|
||||
# f.write(base64.b64decode(base64_image))
|
||||
|
||||
# return image_path
|
||||
return ""
|
||||
return image_path
|
||||
|
||||
|
||||
async def get_image_from_pexels(prompt: str, output_directory: str) -> str:
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class LLMTableDataModelWithValidation(LLMTableDataModel):
|
|||
|
||||
class LLMTableModelWithValidation(LLMTableModel):
|
||||
name: str = Field(
|
||||
description="Name of the table in less than 8 words",
|
||||
description="Name of the table in about 8 words",
|
||||
min_length=10,
|
||||
max_length=50,
|
||||
)
|
||||
|
|
@ -62,20 +62,20 @@ class LLMTableModelWithValidation(LLMTableModel):
|
|||
|
||||
class LLMHeadingModelWithValidation(LLMHeadingModel):
|
||||
heading: str = Field(
|
||||
description="Item heading in less than 6 words",
|
||||
description="Item heading in about 6 words",
|
||||
min_length=10,
|
||||
max_length=40,
|
||||
)
|
||||
description: str = Field(
|
||||
description="Item description in less than 15 words.",
|
||||
description="Item description in about 12 words.",
|
||||
min_length=50,
|
||||
max_length=150,
|
||||
max_length=120,
|
||||
)
|
||||
|
||||
|
||||
class LLMHeadingModelWithImagePromptWithValidation(LLMHeadingModelWithImagePrompt):
|
||||
image_prompt: str = Field(
|
||||
description="Item image prompt in less than 10 words",
|
||||
description="Item image prompt in about 10 words",
|
||||
min_length=10,
|
||||
max_length=100,
|
||||
)
|
||||
|
|
@ -83,7 +83,7 @@ class LLMHeadingModelWithImagePromptWithValidation(LLMHeadingModelWithImagePromp
|
|||
|
||||
class LLMHeadingModelWithIconQueryWithValidation(LLMHeadingModelWithIconQuery):
|
||||
icon_query: str = Field(
|
||||
description="Item icon query in less than 4 words",
|
||||
description="Item icon query in about 4 words",
|
||||
min_length=10,
|
||||
max_length=40,
|
||||
)
|
||||
|
|
@ -91,7 +91,7 @@ class LLMHeadingModelWithIconQueryWithValidation(LLMHeadingModelWithIconQuery):
|
|||
|
||||
class LLMSlideContentModelWithValidation(LLMSlideContentModel):
|
||||
title: str = Field(
|
||||
description="Slide title in less than 8 words",
|
||||
description="Slide title in about 8 words",
|
||||
min_length=10,
|
||||
max_length=80,
|
||||
)
|
||||
|
|
@ -99,12 +99,12 @@ class LLMSlideContentModelWithValidation(LLMSlideContentModel):
|
|||
|
||||
class LLMType1ContentWithValidation(LLMType1Content):
|
||||
body: str = Field(
|
||||
description="Slide content summary in less than 30 words.",
|
||||
description="Slide content summary in about 30 words.",
|
||||
min_length=50,
|
||||
max_length=300,
|
||||
)
|
||||
image_prompt: str = Field(
|
||||
description="Slide image prompt in less than 5 words",
|
||||
description="Slide image prompt in about 5 words",
|
||||
min_length=10,
|
||||
max_length=30,
|
||||
)
|
||||
|
|
@ -125,7 +125,7 @@ class LLMType3ContentWithValidation(LLMType3Content):
|
|||
max_length=3,
|
||||
)
|
||||
image_prompt: str = Field(
|
||||
description="Slide image prompt in less than 5 words",
|
||||
description="Slide image prompt in about 5 words",
|
||||
min_length=10,
|
||||
max_length=30,
|
||||
)
|
||||
|
|
@ -141,7 +141,7 @@ class LLMType4ContentWithValidation(LLMType4Content):
|
|||
|
||||
class LLMType5ContentWithValidation(LLMType5Content):
|
||||
body: str = Field(
|
||||
description="Slide content summary in less than 30 words.",
|
||||
description="Slide content summary in about 30 words.",
|
||||
min_length=50,
|
||||
max_length=300,
|
||||
)
|
||||
|
|
@ -150,7 +150,7 @@ class LLMType5ContentWithValidation(LLMType5Content):
|
|||
|
||||
class LLMType6ContentWithValidation(LLMType6Content):
|
||||
description: str = Field(
|
||||
description="Slide content summary in less than 20 words.",
|
||||
description="Slide content summary in about 20 words.",
|
||||
min_length=50,
|
||||
max_length=300,
|
||||
)
|
||||
|
|
@ -171,7 +171,7 @@ class LLMType7ContentWithValidation(LLMType7Content):
|
|||
|
||||
class LLMType8ContentWithValidation(LLMType8Content):
|
||||
description: str = Field(
|
||||
description="Slide content summary in less than 20 words.",
|
||||
description="Slide content summary in about 20 words.",
|
||||
min_length=50,
|
||||
max_length=300,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ fsspec==2025.3.2
|
|||
google-ai-generativelanguage==0.6.18
|
||||
google-api-core==2.24.2
|
||||
google-auth==2.40.1
|
||||
google-genai==1.23.0
|
||||
googleapis-common-protos==1.70.0
|
||||
greenlet==3.2.2
|
||||
grpcio==1.72.0rc1
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ const Header = ({
|
|||
if (response.ok) {
|
||||
const { path: pdfPath } = await response.json();
|
||||
const staticFileUrl = getStaticFileUrl(pdfPath);
|
||||
window.open(staticFileUrl, '_self');
|
||||
window.open(staticFileUrl, '_blank');
|
||||
} else {
|
||||
throw new Error("Failed to export PDF");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ export async function POST(request: Request) {
|
|||
LLM: userConfig.LLM || existingConfig.LLM,
|
||||
OPENAI_API_KEY: userConfig.OPENAI_API_KEY || existingConfig.OPENAI_API_KEY,
|
||||
GOOGLE_API_KEY: userConfig.GOOGLE_API_KEY || existingConfig.GOOGLE_API_KEY,
|
||||
OLLAMA_MODEL: userConfig.OLLAMA_MODEL || existingConfig.OLLAMA_MODEL,
|
||||
MODEL: userConfig.MODEL || existingConfig.MODEL,
|
||||
LLM_PROVIDER_URL: userConfig.LLM_PROVIDER_URL || existingConfig.LLM_PROVIDER_URL,
|
||||
LLM_API_KEY: userConfig.LLM_API_KEY || existingConfig.LLM_API_KEY,
|
||||
PEXELS_API_KEY: userConfig.PEXELS_API_KEY || existingConfig.PEXELS_API_KEY,
|
||||
}
|
||||
fs.writeFileSync(userConfigPath, JSON.stringify(mergedConfig))
|
||||
|
|
|
|||
|
|
@ -2,13 +2,29 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import Header from "../dashboard/components/Header";
|
||||
import Wrapper from "@/components/Wrapper";
|
||||
import { Settings, Key, Loader2 } from 'lucide-react';
|
||||
import { Settings, Key, Loader2, Check, ChevronsUpDown } from 'lucide-react';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { RootState } from "@/store/store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { handleSaveLLMConfig } from "@/utils/storeHelpers";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
const PROVIDER_CONFIGS: Record<string, ProviderConfig> = {
|
||||
openai: {
|
||||
|
|
@ -55,14 +71,22 @@ const SettingsPage = () => {
|
|||
done: false,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [openModelSelect, setOpenModelSelect] = useState(false);
|
||||
const [useCustomOllamaUrl, setUseCustomOllamaUrl] = useState<boolean>(false);
|
||||
|
||||
const api_key_changed = (apiKey: string) => {
|
||||
const api_key_changed = (apiKey: string, field?: string) => {
|
||||
if (llmConfig.LLM === 'openai') {
|
||||
setLlmConfig({ ...llmConfig, OPENAI_API_KEY: apiKey });
|
||||
} else if (llmConfig.LLM === 'google') {
|
||||
setLlmConfig({ ...llmConfig, GOOGLE_API_KEY: apiKey });
|
||||
} else if (llmConfig.LLM === 'ollama') {
|
||||
setLlmConfig({ ...llmConfig, PEXELS_API_KEY: apiKey });
|
||||
if (field === 'pexels') {
|
||||
setLlmConfig({ ...llmConfig, PEXELS_API_KEY: apiKey });
|
||||
} else if (field === 'ollama_url') {
|
||||
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: apiKey });
|
||||
} else if (field === 'ollama_api_key') {
|
||||
setLlmConfig({ ...llmConfig, LLM_API_KEY: apiKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +111,7 @@ const SettingsPage = () => {
|
|||
}
|
||||
}
|
||||
try {
|
||||
await handleSaveLLMConfig(llmConfig);
|
||||
await handleSaveLLMConfig(llmConfig, useCustomOllamaUrl);
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Configuration saved successfully',
|
||||
|
|
@ -116,7 +140,7 @@ const SettingsPage = () => {
|
|||
return new Promise((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/ppt/ollama/pull-model?name=${llmConfig.OLLAMA_MODEL}`);
|
||||
const response = await fetch(`/api/v1/ppt/ollama/pull-model?name=${llmConfig.MODEL}`);
|
||||
if (response.status === 200) {
|
||||
|
||||
const data = await response.json();
|
||||
|
|
@ -163,6 +187,14 @@ const SettingsPage = () => {
|
|||
}
|
||||
}, [userConfigState.llm_config.LLM]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!useCustomOllamaUrl) {
|
||||
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: undefined, LLM_API_KEY: undefined });
|
||||
} else {
|
||||
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: 'http://localhost:11434', LLM_API_KEY: '' });
|
||||
}
|
||||
}, [useCustomOllamaUrl]);
|
||||
|
||||
if (!canChangeKeys) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -262,70 +294,90 @@ const SettingsPage = () => {
|
|||
</label>
|
||||
<div className="w-full">
|
||||
{ollamaModels.length > 0 ? (
|
||||
<Select value={llmConfig.OLLAMA_MODEL} onValueChange={(value) => setLlmConfig({ ...llmConfig, OLLAMA_MODEL: value })}>
|
||||
<SelectTrigger className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Popover open={openModelSelect} onOpenChange={setOpenModelSelect}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openModelSelect}
|
||||
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-6 h-6 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
src={ollamaModels.find(m => m.value === llmConfig.OLLAMA_MODEL)?.icon}
|
||||
alt={`${llmConfig.OLLAMA_MODEL} icon`}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
{llmConfig.MODEL && (
|
||||
<div className="w-6 h-6 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
src={ollamaModels.find(m => m.value === llmConfig.MODEL)?.icon}
|
||||
alt={`${llmConfig.MODEL} icon`}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{llmConfig.OLLAMA_MODEL ? (
|
||||
ollamaModels.find(m => m.value === llmConfig.OLLAMA_MODEL)?.label || llmConfig.OLLAMA_MODEL
|
||||
{llmConfig.MODEL ? (
|
||||
ollamaModels.find(m => m.value === llmConfig.MODEL)?.label || llmConfig.MODEL
|
||||
) : (
|
||||
'Select a model'
|
||||
)}
|
||||
</span>
|
||||
{llmConfig.OLLAMA_MODEL && (
|
||||
{llmConfig.MODEL && (
|
||||
<span className="text-xs text-gray-500 bg-gray-100 rounded-full px-2 py-1">
|
||||
{ollamaModels.find(m => m.value === llmConfig.OLLAMA_MODEL)?.size}
|
||||
{ollamaModels.find(m => m.value === llmConfig.MODEL)?.size}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-80">
|
||||
<div className="p-2">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 pt-3 px-2">
|
||||
Available Models
|
||||
</div>
|
||||
{ollamaModels.map((model, index) => (
|
||||
<SelectItem
|
||||
key={index}
|
||||
value={model.value}
|
||||
className="relative cursor-pointer rounded-md py-3 hover:bg-gray-50 focus:bg-gray-50 focus:outline-none transition-colors"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
src={model.icon}
|
||||
alt={`${model.label} icon`}
|
||||
className=" rounded-sm"
|
||||
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start" style={{ width: 'var(--radix-popover-trigger-width)' }}>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search model..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{ollamaModels.map((model, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={model.value}
|
||||
onSelect={(value) => {
|
||||
setLlmConfig({ ...llmConfig, MODEL: value });
|
||||
setOpenModelSelect(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
llmConfig.MODEL === model.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 capitalize">
|
||||
{model.label}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
|
||||
{model.size}
|
||||
</span>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
src={model.icon}
|
||||
alt={`${model.label} icon`}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 capitalize">
|
||||
{model.label}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
|
||||
{model.size}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 leading-relaxed">
|
||||
{model.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 leading-relaxed">
|
||||
{model.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<div className="w-full border border-gray-300 rounded-lg p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
|
|
@ -344,6 +396,62 @@ const SettingsPage = () => {
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Custom Ollama URL Configuration */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Use custom Ollama URL
|
||||
</label>
|
||||
<Switch
|
||||
checked={useCustomOllamaUrl}
|
||||
onCheckedChange={setUseCustomOllamaUrl}
|
||||
/>
|
||||
</div>
|
||||
{useCustomOllamaUrl && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ollama URL
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Enter your Ollama URL"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={llmConfig.LLM_PROVIDER_URL || ''}
|
||||
onChange={(e) => api_key_changed(e.target.value, 'ollama_url')}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
Change this if you are using a custom Ollama instance
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ollama API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Enter your Ollama API key"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={llmConfig.LLM_API_KEY || ''}
|
||||
onChange={(e) => api_key_changed(e.target.value, 'ollama_api_key')}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
Provide this if you are using a custom Ollama instance
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Pexels API Key (required for images)
|
||||
|
|
@ -355,12 +463,12 @@ const SettingsPage = () => {
|
|||
placeholder="Enter your Pexels API key"
|
||||
className="flex-1 px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={llmConfig.PEXELS_API_KEY || ''}
|
||||
onChange={(e) => api_key_changed(e.target.value)}
|
||||
onChange={(e) => api_key_changed(e.target.value, 'pexels')}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSaveConfig}
|
||||
disabled={isLoading || !llmConfig.OLLAMA_MODEL}
|
||||
className={`px-4 py-2 rounded-lg transition-colors ${isLoading || !llmConfig.OLLAMA_MODEL
|
||||
disabled={isLoading || !llmConfig.MODEL}
|
||||
className={`px-4 py-2 rounded-lg transition-colors ${isLoading || !llmConfig.MODEL
|
||||
? 'bg-gray-400 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700'
|
||||
} text-white`}
|
||||
|
|
@ -374,7 +482,7 @@ const SettingsPage = () => {
|
|||
}
|
||||
</div>
|
||||
) : (
|
||||
!llmConfig.OLLAMA_MODEL ? 'Select Model' : 'Save'
|
||||
!llmConfig.MODEL ? 'Select Model' : 'Save'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@ export function StoreInitializer({ children }: { children: React.ReactNode }) {
|
|||
llmConfig.LLM = 'openai';
|
||||
}
|
||||
dispatch(setLLMConfig(llmConfig));
|
||||
const isValid = hasValidLLMConfig(llmConfig);
|
||||
const isValid = hasValidLLMConfig(llmConfig, false);
|
||||
if (isValid) {
|
||||
// Check if the selected Ollama model is pulled
|
||||
if (llmConfig.LLM === 'ollama') {
|
||||
const isPulled = await checkIfSelectedOllamaModelIsPulled(llmConfig.OLLAMA_MODEL);
|
||||
const isPulled = await checkIfSelectedOllamaModelIsPulled(llmConfig.MODEL);
|
||||
if (!isPulled) {
|
||||
router.push('/');
|
||||
setLoadingToFalseAfterNavigatingTo('/');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { Info, ExternalLink, PlayCircle, Loader2 } from "lucide-react";
|
||||
import { Info, ExternalLink, PlayCircle, Loader2, Check, ChevronsUpDown } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Accordion,
|
||||
|
|
@ -14,6 +14,22 @@ import { useSelector } from "react-redux";
|
|||
import { RootState } from "@/store/store";
|
||||
import { handleSaveLLMConfig } from "@/utils/storeHelpers";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "./ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "./ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Switch } from "./ui/switch";
|
||||
|
||||
interface ModelOption {
|
||||
value: string;
|
||||
|
|
@ -171,16 +187,24 @@ export default function Home() {
|
|||
done: false,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [openModelSelect, setOpenModelSelect] = useState(false);
|
||||
const [useCustomOllamaUrl, setUseCustomOllamaUrl] = useState<boolean>(false);
|
||||
|
||||
const canChangeKeys = config.can_change_keys;
|
||||
|
||||
const api_key_changed = (newApiKey: string) => {
|
||||
const api_key_changed = (newApiKey: string, field?: string) => {
|
||||
if (llmConfig.LLM === 'openai') {
|
||||
setLlmConfig({ ...llmConfig, OPENAI_API_KEY: newApiKey });
|
||||
} else if (llmConfig.LLM === 'google') {
|
||||
setLlmConfig({ ...llmConfig, GOOGLE_API_KEY: newApiKey });
|
||||
} else if (llmConfig.LLM === 'ollama') {
|
||||
setLlmConfig({ ...llmConfig, PEXELS_API_KEY: newApiKey });
|
||||
if (field === 'pexels') {
|
||||
setLlmConfig({ ...llmConfig, PEXELS_API_KEY: newApiKey });
|
||||
} else if (field === 'ollama_url') {
|
||||
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: newApiKey });
|
||||
} else if (field === 'ollama_api_key') {
|
||||
setLlmConfig({ ...llmConfig, LLM_API_KEY: newApiKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +229,7 @@ export default function Home() {
|
|||
}
|
||||
}
|
||||
try {
|
||||
await handleSaveLLMConfig(llmConfig);
|
||||
await handleSaveLLMConfig(llmConfig, useCustomOllamaUrl);
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Configuration saved successfully',
|
||||
|
|
@ -234,7 +258,7 @@ export default function Home() {
|
|||
return new Promise((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/v1/ppt/ollama/pull-model?name=${llmConfig.OLLAMA_MODEL}`);
|
||||
const response = await fetch(`/api/v1/ppt/ollama/pull-model?name=${llmConfig.MODEL}`);
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
|
||||
|
|
@ -277,6 +301,15 @@ export default function Home() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!useCustomOllamaUrl) {
|
||||
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: undefined, LLM_API_KEY: undefined });
|
||||
} else {
|
||||
setLlmConfig({ ...llmConfig, LLM_PROVIDER_URL: 'http://localhost:11434', LLM_API_KEY: '' });
|
||||
}
|
||||
}, [useCustomOllamaUrl]);
|
||||
|
||||
|
||||
if (!canChangeKeys) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -355,70 +388,90 @@ export default function Home() {
|
|||
</label>
|
||||
<div className="w-full">
|
||||
{ollamaModels.length > 0 ? (
|
||||
<Select value={llmConfig.OLLAMA_MODEL} onValueChange={(value) => setLlmConfig({ ...llmConfig, OLLAMA_MODEL: value })}>
|
||||
<SelectTrigger className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<Popover open={openModelSelect} onOpenChange={setOpenModelSelect}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openModelSelect}
|
||||
className="w-full h-12 px-4 py-4 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors hover:border-gray-400 justify-between"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-6 h-6 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
src={ollamaModels.find(m => m.value === llmConfig.OLLAMA_MODEL)?.icon}
|
||||
alt={`${llmConfig.OLLAMA_MODEL} icon`}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
{llmConfig.MODEL && (
|
||||
<div className="w-6 h-6 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
src={ollamaModels.find(m => m.value === llmConfig.MODEL)?.icon}
|
||||
alt={`${llmConfig.MODEL} icon`}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{llmConfig.OLLAMA_MODEL ? (
|
||||
ollamaModels.find(m => m.value === llmConfig.OLLAMA_MODEL)?.label || llmConfig.OLLAMA_MODEL
|
||||
{llmConfig.MODEL ? (
|
||||
ollamaModels.find(m => m.value === llmConfig.MODEL)?.label || llmConfig.MODEL
|
||||
) : (
|
||||
'Select a model'
|
||||
)}
|
||||
</span>
|
||||
{llmConfig.OLLAMA_MODEL && (
|
||||
{llmConfig.MODEL && (
|
||||
<span className="text-xs text-gray-500 bg-gray-100 rounded-full px-2 py-1">
|
||||
{ollamaModels.find(m => m.value === llmConfig.OLLAMA_MODEL)?.size}
|
||||
{ollamaModels.find(m => m.value === llmConfig.MODEL)?.size}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-80">
|
||||
<div className="p-2">
|
||||
<div className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3 pt-3 px-2">
|
||||
Available Models
|
||||
</div>
|
||||
{ollamaModels.map((model, index) => (
|
||||
<SelectItem
|
||||
key={index}
|
||||
value={model.value}
|
||||
className="relative cursor-pointer rounded-md py-3 hover:bg-gray-50 focus:bg-gray-50 focus:outline-none transition-colors"
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
src={model.icon}
|
||||
alt={`${model.label} icon`}
|
||||
className=" rounded-sm"
|
||||
<ChevronsUpDown className="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start" style={{ width: 'var(--radix-popover-trigger-width)' }}>
|
||||
<Command>
|
||||
<CommandInput placeholder="Search model..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No model found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{ollamaModels.map((model, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={model.value}
|
||||
onSelect={(value) => {
|
||||
setLlmConfig({ ...llmConfig, MODEL: value });
|
||||
setOpenModelSelect(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
llmConfig.MODEL === model.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 capitalize">
|
||||
{model.label}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
|
||||
{model.size}
|
||||
</span>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<img
|
||||
src={model.icon}
|
||||
alt={`${model.label} icon`}
|
||||
className="rounded-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 capitalize">
|
||||
{model.label}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 bg-gray-100 px-2 py-1 rounded-full">
|
||||
{model.size}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 leading-relaxed">
|
||||
{model.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 leading-relaxed">
|
||||
{model.description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<div className="w-full border border-gray-300 rounded-lg p-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
|
|
@ -437,6 +490,59 @@ export default function Home() {
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4 bg-green-50 p-2 rounded-sm">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Use custom Ollama URL
|
||||
</label>
|
||||
<Switch
|
||||
checked={useCustomOllamaUrl}
|
||||
onCheckedChange={setUseCustomOllamaUrl}
|
||||
/>
|
||||
</div>
|
||||
{useCustomOllamaUrl && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ollama URL
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Enter your Ollama URL"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={llmConfig.LLM_PROVIDER_URL || ''}
|
||||
onChange={(e) => api_key_changed(e.target.value, 'ollama_url')}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
Change this if you are using a custom Ollama instance
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ollama API Key
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Enter your Ollama API key"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={llmConfig.LLM_API_KEY || ''}
|
||||
onChange={(e) => api_key_changed(e.target.value, 'ollama_api_key')}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
<span className="block w-1 h-1 rounded-full bg-gray-400"></span>
|
||||
Provide this if you are using a custom Ollama instance
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Pexels API Key (required for images)
|
||||
|
|
@ -448,7 +554,7 @@ export default function Home() {
|
|||
placeholder="Enter your Pexels API key"
|
||||
className="w-full px-4 py-2.5 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors"
|
||||
value={llmConfig.PEXELS_API_KEY || ''}
|
||||
onChange={(e) => api_key_changed(e.target.value)}
|
||||
onChange={(e) => api_key_changed(e.target.value, 'pexels')}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-gray-500 flex items-center gap-2">
|
||||
|
|
@ -468,7 +574,7 @@ export default function Home() {
|
|||
Selected Models
|
||||
</h3>
|
||||
<p className="text-sm text-blue-700">
|
||||
Using {llmConfig.LLM === 'ollama' ? llmConfig.OLLAMA_MODEL ?? '_____' : PROVIDER_CONFIGS[llmConfig.LLM!].textModels[0].label} for text
|
||||
Using {llmConfig.LLM === 'ollama' ? llmConfig.MODEL ?? '_____' : PROVIDER_CONFIGS[llmConfig.LLM!].textModels[0].label} for text
|
||||
generation and {PROVIDER_CONFIGS[llmConfig.LLM!].imageModels[0].label} for
|
||||
images
|
||||
</p>
|
||||
|
|
@ -544,7 +650,7 @@ export default function Home() {
|
|||
}
|
||||
</div>
|
||||
) : (
|
||||
llmConfig.LLM === 'ollama' && !llmConfig.OLLAMA_MODEL
|
||||
llmConfig.LLM === 'ollama' && !llmConfig.MODEL
|
||||
? 'Please Select a Model'
|
||||
: 'Save Configuration'
|
||||
)}
|
||||
|
|
|
|||
4
servers/nextjs/types/global.d.ts
vendored
4
servers/nextjs/types/global.d.ts
vendored
|
|
@ -18,5 +18,7 @@ interface LLMConfig {
|
|||
OPENAI_API_KEY?: string;
|
||||
GOOGLE_API_KEY?: string;
|
||||
PEXELS_API_KEY?: string;
|
||||
OLLAMA_MODEL?: string;
|
||||
LLM_PROVIDER_URL?: string;
|
||||
LLM_API_KEY?: string;
|
||||
MODEL?: string;
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { setLLMConfig } from "@/store/slices/userConfig";
|
||||
import { store } from "@/store/store";
|
||||
|
||||
export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
|
||||
if (!hasValidLLMConfig(llmConfig)) {
|
||||
export const handleSaveLLMConfig = async (llmConfig: LLMConfig, useCustomOllamaUrl: boolean) => {
|
||||
if (!hasValidLLMConfig(llmConfig, useCustomOllamaUrl)) {
|
||||
throw new Error('API key cannot be empty');
|
||||
}
|
||||
|
||||
|
|
@ -14,17 +14,21 @@ export const handleSaveLLMConfig = async (llmConfig: LLMConfig) => {
|
|||
store.dispatch(setLLMConfig(llmConfig));
|
||||
}
|
||||
|
||||
export const hasValidLLMConfig = (llmConfig: LLMConfig) => {
|
||||
export const hasValidLLMConfig = (llmConfig: LLMConfig, useCustomOllamaUrl: boolean) => {
|
||||
if (!llmConfig.LLM) return false;
|
||||
const OPENAI_API_KEY = llmConfig.OPENAI_API_KEY;
|
||||
const GOOGLE_API_KEY = llmConfig.GOOGLE_API_KEY;
|
||||
const OLLAMA_MODEL = llmConfig.OLLAMA_MODEL;
|
||||
const MODEL = llmConfig.MODEL;
|
||||
const PEXELS_API_KEY = llmConfig.PEXELS_API_KEY;
|
||||
|
||||
const isOllamaBaseConfigValid = PEXELS_API_KEY !== '' && PEXELS_API_KEY !== null && PEXELS_API_KEY !== undefined && MODEL !== '' && MODEL !== null && MODEL !== undefined;
|
||||
|
||||
return llmConfig.LLM === 'openai' ?
|
||||
OPENAI_API_KEY !== '' && OPENAI_API_KEY !== null && OPENAI_API_KEY !== undefined :
|
||||
llmConfig.LLM === 'google' ?
|
||||
GOOGLE_API_KEY !== '' && GOOGLE_API_KEY !== null && GOOGLE_API_KEY !== undefined :
|
||||
llmConfig.LLM === 'ollama' ?
|
||||
PEXELS_API_KEY !== '' && PEXELS_API_KEY !== null && PEXELS_API_KEY !== undefined && OLLAMA_MODEL !== '' && OLLAMA_MODEL !== null && OLLAMA_MODEL !== undefined :
|
||||
false;
|
||||
useCustomOllamaUrl ?
|
||||
isOllamaBaseConfigValid && llmConfig.LLM_PROVIDER_URL !== '' && llmConfig.LLM_PROVIDER_URL !== null && llmConfig.LLM_PROVIDER_URL !== undefined && llmConfig.LLM_API_KEY !== '' && llmConfig.LLM_API_KEY !== null && llmConfig.LLM_API_KEY !== undefined :
|
||||
isOllamaBaseConfigValid : false;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue