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
3 changes: 3 additions & 0 deletions .github/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
- name: "component: AI File Context"
color: "000"
description: "Issues related to the AI File Context component"
- name: "component: AI Models"
color: "000"
description: "Issues related to the AI Models component"
- name: "component: AI Chat"
color: "000"
description: "Issues related to the Chat component"
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.5-alpha] - 2025-07-19

### Added

- New methods in AIProvider base class:
- Add DefaultServerUrl property
- Added CallApi method to AIProvider base class supporting GET/POST/DELETE/PATCH
- Added RetrieveAvailableModels method to AIProvider base class with default to empty list
- Implemented RetrieveAvailableModels, CallApi and DefaultServerUrl to existing providers (MistralAIProvider, OpenAIProvider, and DeepSeekProvider).
- New AIModelsComponent component under SmartHopper > AI categories that uses provider's RetrieveAvailableModels() to fetch model list.

### Changed

- Update providersResources access modifiers from public to internal
- Clean up AboutDialog by removing MathJax attribution
- Moved provider selection logic from AIProviderComponentBase to AIProviderComponentBase
- Moved InputsChanged method with override for including HasProviderChanged from AIStatefulAsyncComponentBase to AIProviderComponentBase

### Removed

- Removed MathJax support from chat UI since it was not properly implemented and was generating security warnings on GitHub.

## [0.3.4-alpha] - 2025-07-11

### Added
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SmartHopper - AI-Powered Grasshopper3D Plugin

[![Version](https://img.shields.io/badge/version-0%2E3%2E4--alpha-orange)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Version](https://img.shields.io/badge/version-0%2E3%2E5--alpha-orange)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Status](https://img.shields.io/badge/status-Alpha-orange)](https://github.com/architects-toolkit/SmartHopper/releases)
[![Test results](https://img.shields.io/github/actions/workflow/status/architects-toolkit/SmartHopper/.github/workflows/ci-dotnet-tests.yml?label=.NET%20CI&logo=dotnet)](https://github.com/architects-toolkit/SmartHopper/actions/workflows/ci-dotnet-tests.yml)
[![Grasshopper](https://img.shields.io/badge/plugin_for-Grasshopper3D-darkgreen?logo=rhinoceros)](https://www.rhino3d.com/)
Expand Down Expand Up @@ -79,6 +79,7 @@ After installation, all SmartHopper components will be available in the Grasshop
| AI JSON Generate (AiJsonGenerate)<br><sub>Generate an AI response in strict JSON output</sub> | ⚪ | - | - | - |
| AI GroupTitle (AiGroupTitle)<br><sub>Group components and set a meaningful title to the group</sub> | ⚪ | - | - | - |
| AI File Context (AiFileContext)<br><sub>Set a context for the current document</sub> | ⚪ | 🟡 | 🟠 | 🟢 |
| AI Models (AiModels)<br><sub>Retrieve the list of available models for a specific provider</sub> | ⚪ | 🟡 | 🟠 | 🟢 |
| JSON schema (JsonSchema)<br><sub>Set a JSON schema for the AI component</sub> | ⚪ | - | - | - |
| JSON object (JsonObject)<br><sub>Set a JSON object for the definition of the JSON Schema</sub> | ⚪ | - | - | - |
| JSON array (JsonArray)<br><sub>Set a JSON array for the definition of the JSON Schema</sub> | ⚪ | - | - | - |
Expand Down
2 changes: 1 addition & 1 deletion Solution.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<SolutionVersion>0.3.4-alpha</SolutionVersion>
<SolutionVersion>0.3.5-alpha</SolutionVersion>
</PropertyGroup>
</Project>
5 changes: 2 additions & 3 deletions src/SmartHopper.Components/AI/AIChatComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,12 @@ protected override void RegisterAdditionalOutputParams(GH_OutputParamManager pMa
/// <summary>
/// Gets the system prompt from the component.
/// </summary>
/// <returns>The system prompt.</returns>
/// <inheritdoc/>
protected override void SolveInstance(IGH_DataAccess DA)
{
string systemPrompt = null;
DA.GetData("Instructions", ref systemPrompt);
SetSystemPrompt(systemPrompt);
this.SetSystemPrompt(systemPrompt);

base.SolveInstance(DA);
}
Expand Down Expand Up @@ -230,7 +229,7 @@ public override async System.Threading.Tasks.Task DoWorkAsync(CancellationToken

// Get the actual provider name to use
string actualProvider = this.component.GetActualProviderName();
Debug.WriteLine($"[AIChatWorker] Using Provider: {actualProvider} (Selected: {this.component._aiProvider})");
Debug.WriteLine($"[AIChatWorker] Using Provider: {actualProvider} (Selected: {this.component.GetActualProviderName()})");

// Create a web chat worker
var chatWorker = WebChatUtils.CreateWebChatWorker(
Expand Down
192 changes: 192 additions & 0 deletions src/SmartHopper.Components/AI/AIModelsComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* SmartHopper - AI-powered Grasshopper Plugin
* Copyright (C) 2025 Marc Roca Musach
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*/

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Data;
using Grasshopper.Kernel.Types;
using SmartHopper.Components.Properties;
using SmartHopper.Core.ComponentBase;

namespace SmartHopper.Components.AI
{
/// <summary>
/// Grasshopper component for retrieving available AI models from the selected provider.
/// </summary>
public class AIModelsComponent : AIProviderComponentBase
{
/// <summary>
/// Gets the unique ID for this component. Do not change this ID after release.
/// </summary>
public override Guid ComponentGuid => new Guid("E5834FB2-4CC0-4D0A-8AB3-EF2345678901");

/// <summary>
/// Gets the icon for this component.
/// </summary>
protected override Bitmap Icon => Resources.smarthopper;

/// <summary>
/// Gets the exposure level of this component in the ribbon.
/// </summary>
public override GH_Exposure Exposure => GH_Exposure.primary;

/// <summary>
/// Initializes a new instance of the AIModelsComponent class.
/// </summary>
public AIModelsComponent()
: base(
"AI Models",
"AIModels",
"Retrieve the list of available models from the selected AI provider.",
"SmartHopper", "AI")
{
this.RunOnlyOnInputChanges = false;
}

/// <summary>
/// Registers the output parameters for this component.
/// </summary>
/// <param name="pManager">The parameter manager.</param>
protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
{
// Override to suppress the Metrics output
pManager.AddTextParameter("Models", "M", "List of available model names from the selected provider", GH_ParamAccess.tree);
}

/// <summary>
/// Registers additional input parameters (required by StatefulAsyncComponentBase).
/// </summary>
/// <param name="pManager">The parameter manager to register inputs with.</param>
protected override void RegisterAdditionalInputParams(GH_Component.GH_InputParamManager pManager)
{
// No additional input parameters needed for this component
}

/// <summary>
/// Registers additional output parameters (required by StatefulAsyncComponentBase).
/// </summary>
/// <param name="pManager">The parameter manager to register outputs with.</param>
protected override void RegisterAdditionalOutputParams(GH_Component.GH_OutputParamManager pManager)
{
// No additional output parameters needed for this component
}

/// <summary>
/// Creates the async worker for this component.
/// </summary>
/// <param name="progressReporter">Progress reporter callback.</param>
/// <returns>The async worker instance.</returns>
protected override AsyncWorkerBase CreateWorker(Action<string> progressReporter)
{
return new AIModelsWorker(this, this.AddRuntimeMessage);
}

/// <summary>
/// Async worker for the AI Models component.
/// </summary>
public class AIModelsWorker : AsyncWorkerBase
{
private readonly AIModelsComponent _parent;
private readonly Dictionary<string, object> _result = new Dictionary<string, object>();

public AIModelsWorker(AIModelsComponent parent, Action<GH_RuntimeMessageLevel, string> addRuntimeMessage)
: base(parent, addRuntimeMessage)
{
this._parent = parent;
}

/// <summary>
/// Gathers input from the component.
/// </summary>
/// <param name="DA">Data access object.</param>
/// <param name="message">Output message.</param>
public override void GatherInput(IGH_DataAccess DA)
{
// No inputs to gather for this component
}

/// <summary>
/// Performs the async work to retrieve available models.
/// </summary>
/// <param name="message">Output message.</param>
/// <returns>Async task.</returns>
public override async Task DoWorkAsync(CancellationToken token)
{
try
{
// Get the current AI provider
var provider = this._parent.GetCurrentAIProvider();
if (provider == null)
{
this._result["Success"] = false;
this._result["Error"] = "No AI provider selected or available";
return;
}

// Retrieve available models
var models = await provider.RetrieveAvailableModels();
if (models == null || !models.Any())
{
this._result["Success"] = false;
this._result["Error"] = "No models available from the selected provider";
return;
}

// Convert to GH_Structure for output
var tree = new GH_Structure<GH_String>();
var path = new GH_Path(0);
foreach (var model in models)
{
tree.Append(new GH_String(model), path);
}

this._result["Models"] = tree;
this._result["Success"] = true;
}
catch (Exception ex)
{
this._result["Success"] = false;
this._result["Error"] = ex.Message;
}
}

/// <summary>
/// Sets the output from the async work.
/// </summary>
/// <param name="DA">Data access object.</param>
/// <param name="message">Output message.</param>
public override void SetOutput(IGH_DataAccess DA, out string message)
{
if (this._result.TryGetValue("Success", out var success) && (bool)success)
{
if (this._result.TryGetValue("Models", out var models) && models is GH_Structure<GH_String> tree)
{
this._parent.SetPersistentOutput("Models", tree, DA);
message = "Models output set successfully";
}
else
{
message = "Error: No models data available";
}
}
else
{
this._parent.AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Error occurred while retrieving models");
message = "Error occurred while retrieving models";
}
}
}
}
}
60 changes: 33 additions & 27 deletions src/SmartHopper.Core/ComponentBase/AIComponentAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,83 +23,89 @@ namespace SmartHopper.Core.ComponentBase
/// </summary>
public class AIComponentAttributes : GH_ComponentAttributes
{
private readonly AIStatefulAsyncComponentBase _owner;
private const int BADGE_SIZE = 16; // Size of the provider logo badge
private const float MIN_ZOOM_THRESHOLD = 0.5f; // Minimum zoom level to show the badge
private const int PROVIDER_STRIP_HEIGHT = 20; // Height of the provider strip
private readonly AIProviderComponentBase owner;
private const int BADGESIZE = 16; // Size of the provider logo badge
private const float MINZOOMTHRESHOLD = 0.5f; // Minimum zoom level to show the badge
private const int PROVIDERSTRIPHEIGHT = 20; // Height of the provider strip

/// <summary>
/// Creates a new instance of AIComponentAttributes
/// Initializes a new instance of the <see cref="AIComponentAttributes"/> class.
/// Creates a new instance of AIComponentAttributes.
/// </summary>
/// <param name="owner">The AI component that owns these attributes</param>
public AIComponentAttributes(AIStatefulAsyncComponentBase owner) : base(owner)
/// <param name="owner">The AI component that owns these attributes.</param>
public AIComponentAttributes(AIProviderComponentBase owner)
: base(owner)
{
_owner = owner;
this.owner = owner;
}

/// <summary>
/// Layout the component with additional space for the provider strip
/// Layout the component with additional space for the provider strip.
/// </summary>
protected override void Layout()
{
base.Layout();

// Only extend bounds if we have a valid provider
if (!string.IsNullOrEmpty(_owner._aiProvider))
if (!string.IsNullOrEmpty(this.owner.GetActualProviderName()))
{
var bounds = Bounds;
bounds.Height += PROVIDER_STRIP_HEIGHT;
Bounds = bounds;
var bounds = this.Bounds;
bounds.Height += PROVIDERSTRIPHEIGHT;
this.Bounds = bounds;
}
}

/// <summary>
/// Renders the component with an additional provider strip
/// Renders the component with an additional provider strip.
/// </summary>
/// <param name="canvas">The canvas being rendered to</param>
/// <param name="graphics">The graphics object to use for drawing</param>
/// <param name="channel">The current render channel</param>
/// <param name="canvas">The canvas being rendered to.</param>
/// <param name="graphics">The graphics object to use for drawing.</param>
/// <param name="channel">The current render channel.</param>
protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
{
base.Render(canvas, graphics, channel);

if (channel == GH_CanvasChannel.Objects)
{
// Only render the provider strip if we have a valid provider and we're zoomed in enough
if (string.IsNullOrEmpty(_owner._aiProvider) || canvas.Viewport.Zoom < MIN_ZOOM_THRESHOLD)
if (string.IsNullOrEmpty(this.owner.GetActualProviderName()) || canvas.Viewport.Zoom < MINZOOMTHRESHOLD)
{
return;
}

// Get the actual provider name (resolving Default to the actual provider)
string actualProviderName = _owner._aiProvider;
if (_owner._aiProvider == AIStatefulAsyncComponentBase.DEFAULT_PROVIDER)
string actualProviderName = this.owner.GetActualProviderName();
if (this.owner.GetActualProviderName() == AIProviderComponentBase.DEFAULT_PROVIDER)
{
actualProviderName = SmartHopperSettings.Instance.DefaultAIProvider;
}

// Get the provider icon
var providerIcon = ProviderManager.Instance.GetProvider(actualProviderName)?.Icon;
if (providerIcon == null)
{
return;
}

// Get the bounds of the component
var bounds = Bounds;
var bounds = this.Bounds;

// Calculate strip position at the bottom of the component
var stripRect = new RectangleF(
bounds.Left,
bounds.Bottom - PROVIDER_STRIP_HEIGHT,
bounds.Bottom - PROVIDERSTRIPHEIGHT,
bounds.Width,
PROVIDER_STRIP_HEIGHT);
PROVIDERSTRIPHEIGHT);

// Calculate starting X position to center the pack
var startX = bounds.Left + (bounds.Width - BADGE_SIZE) / 2;
var startX = bounds.Left + ((bounds.Width - BADGESIZE) / 2);

// Calculate icon position within strip
var iconRect = new RectangleF(
startX,
bounds.Bottom - PROVIDER_STRIP_HEIGHT + (PROVIDER_STRIP_HEIGHT - BADGE_SIZE) / 2,
BADGE_SIZE,
BADGE_SIZE);
bounds.Bottom - PROVIDERSTRIPHEIGHT + ((PROVIDERSTRIPHEIGHT - BADGESIZE) / 2),
BADGESIZE,
BADGESIZE);

// Draw the provider icon using GH methods
if (providerIcon != null)
Expand Down
Loading
Loading