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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Summary documentation at `docs/` (linked in README).
- `ModelManager.SetDefault(provider, model, caps, exclusive)` helper to manage per-capability defaults.
- New `AIRuntimeMessage` model to handle information, warning and error messages on AI Call.
- New component badges to visually identify verified and deprecated models.

### Changed

Expand Down
2 changes: 1 addition & 1 deletion docs/Components/ComponentBase/AIProviderComponentBase.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ Expose provider selection via context menu and store the selection so derived co

## Related

- [AIComponentAttributes](../Helpers/AIComponentAttributes.md) – draws a provider logo/badge on the component.
- [AIProviderComponentAttributes](../Helpers/AIProviderComponentAttributes.md) – draws a provider logo/badge on the component.
- [AIStatefulAsyncComponentBase](./StatefulAsyncComponentBase.md) – combines this base with the async state machine.
2 changes: 1 addition & 1 deletion docs/Components/Helpers/AIComponentAttributes.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# AIComponentAttributes
# AIProviderComponentAttributes

Custom Grasshopper attributes that add a provider badge to AI components.

Expand Down
5 changes: 3 additions & 2 deletions docs/Suggestions.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ Breaking changes are acceptable; this is a forward-looking plan.

- [ ] AI Provider streaming of responses
- [ ] AI Provider-side prompt caching
- [ ] AI Call cancellation token
- [ ] AI Call cancellation token, callable by the user in components (cancelled state), webchat (with new ui button to cancel current call) and automatically called on closing webchat dialog or rhino
- [ ] AI Call local caching of responses to avoid recomputation
- [ ] AI Call compatibility with parallel tool calling
- [ ] AI tool to generate Grasshopper definitions in GHJSON format
- [ ] New way to save Grasshopper files in GHJSON format
- [ ] New way to save Grasshopper files in GHJSON format to disk
- [ ] Improve WebChat UI with full html environment, including dynamic loading messages, supporting streaming, different interaction type (image, audio, text, toolcall, toolresult...)
- [ ] Add compatibility in persistant data storage and GhJson to more Grasshopper native data types (colors, points, vectors, lines, plane, circle...)
157 changes: 157 additions & 0 deletions src/SmartHopper.Components.Test/Badges/TestBadgesOneComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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.
*/

/*
* TestBadgesOneComponent: Grasshopper test component to visually verify the
* inline badge rendering. Displays a single sample badge above the component
* using the shared ComponentBadgesAttributes with an override that contributes
* one additional badge and hover label.
*/

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using Grasshopper.Kernel;
using SmartHopper.Core.ComponentBase;

namespace SmartHopper.Components.Test.Badges
{
/// <summary>
/// Test component that renders exactly one badge via custom attributes.
/// </summary>
public class TestBadgesOneComponent : AIStatefulAsyncComponentBase
{
/// <inheritdoc />
public override Guid ComponentGuid => new Guid("2B5E5F5A-6F4D-4C0F-8D99-0E9A04BB33A1");

/// <inheritdoc />
protected override Bitmap Icon => null;

/// <inheritdoc />
public override GH_Exposure Exposure => GH_Exposure.quinary;

/// <summary>
/// Initializes a new instance of the <see cref="TestBadgesOneComponent"/> class.
/// </summary>
public TestBadgesOneComponent()
: base("Test Badges: One", "TBadges1",
"Renders a single sample badge above the component for visual verification.",
"SmartHopper", "Testing")
{
}

/// <inheritdoc />
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
// No custom outputs; base adds Metrics
base.RegisterOutputParams(pManager);
}

/// <inheritdoc />
public override void CreateAttributes()
{
this.m_attributes = new OneBadgeAttributes(this);
}

/// <summary>
/// No-op: badge test component defines no additional inputs.
/// </summary>
/// <param name="pManager">Input param manager.</param>
protected override void RegisterAdditionalInputParams(GH_InputParamManager pManager)
{
// Intentionally empty
}

/// <summary>
/// No-op: badge test component defines no additional outputs.
/// </summary>
/// <param name="pManager">Output param manager.</param>
protected override void RegisterAdditionalOutputParams(GH_OutputParamManager pManager)
{
// Intentionally empty
}

/// <summary>
/// Creates a minimal no-op worker since the component exists only to test inline badge rendering.
/// </summary>
/// <param name="progressReporter">Unused progress reporter.</param>
/// <returns>A worker that performs no computation.</returns>
protected override AsyncWorkerBase CreateWorker(Action<string> progressReporter)
{
return new NoopWorker(this, AddRuntimeMessage);
}

/// <summary>
/// Minimal worker for badge test components. It gathers no input, performs no work,
/// and sets only a lightweight status message so the async pipeline completes.
/// </summary>
private sealed class NoopWorker : AsyncWorkerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="NoopWorker"/> class.
/// </summary>
/// <param name="parent">Component instance.</param>
/// <param name="addRuntimeMessage">Delegate to add runtime messages.</param>
public NoopWorker(GH_Component parent, Action<GH_RuntimeMessageLevel, string> addRuntimeMessage)
: base(parent, addRuntimeMessage)
{
}

/// <inheritdoc />
public override void GatherInput(IGH_DataAccess DA, out int dataCount)
{
dataCount = 0;
}

/// <inheritdoc />
public override Task DoWorkAsync(CancellationToken token)
{
return Task.CompletedTask;
}

/// <inheritdoc />
public override void SetOutput(IGH_DataAccess DA, out string message)
{
message = "Ready";
}
}

/// <summary>
/// Custom attributes contributing one additional badge.
/// </summary>
private class OneBadgeAttributes : ComponentBadgesAttributes
{
private static void DrawSampleBadge(Graphics g, float x, float y)
{
var size = 16f;
using (var bg = new SolidBrush(Color.FromArgb(52, 152, 219))) // blue
using (var pen = new Pen(Color.White, 1.5f))
{
var rect = new RectangleF(x, y, size, size);
g.FillEllipse(bg, rect);
g.DrawEllipse(pen, rect);

// white dot
g.FillEllipse(Brushes.White, rect.X + size * 0.45f, rect.Y + size * 0.45f, size * 0.1f, size * 0.1f);
}
}

public OneBadgeAttributes(AIProviderComponentBase owner) : base(owner) { }

/// <inheritdoc />
protected override IEnumerable<(Action<Graphics, float, float> draw, string label)> GetAdditionalBadges()
{
yield return (DrawSampleBadge, "Sample badge 1");
}
}
}
}
155 changes: 155 additions & 0 deletions src/SmartHopper.Components.Test/Badges/TestBadgesThreeComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* 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.
*/

/*
* TestBadgesThreeComponent: Verifies three inline badges rendering and hover labels.
*/

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using Grasshopper.Kernel;
using SmartHopper.Core.ComponentBase;

namespace SmartHopper.Components.Test.Badges
{
/// <summary>
/// Test component that renders three sample badges via custom attributes.
/// </summary>
public class TestBadgesThreeComponent : AIStatefulAsyncComponentBase
{
/// <inheritdoc />
public override Guid ComponentGuid => new Guid("5D9A39A7-9F1B-4F2E-9D6A-9B9D3F93A0B7");

/// <inheritdoc />
protected override Bitmap Icon => null;

/// <inheritdoc />
public override GH_Exposure Exposure => GH_Exposure.quinary;

public TestBadgesThreeComponent()
: base("Test Badges: Three", "TBadges3",
"Renders three sample badges above the component for visual verification.",
"SmartHopper", "Testing")
{
}

public override void CreateAttributes()
{
this.m_attributes = new ThreeBadgesAttributes(this);
}

/// <summary>
/// No-op: badge test component defines no additional inputs.
/// </summary>
/// <param name="pManager">Input param manager.</param>
protected override void RegisterAdditionalInputParams(GH_InputParamManager pManager)
{
// Intentionally empty
}

/// <summary>
/// No-op: badge test component defines no additional outputs.
/// </summary>
/// <param name="pManager">Output param manager.</param>
protected override void RegisterAdditionalOutputParams(GH_OutputParamManager pManager)
{
// Intentionally empty
}

/// <summary>
/// Creates a minimal no-op worker since the component exists only to test inline badge rendering.
/// </summary>
/// <param name="progressReporter">Unused progress reporter.</param>
/// <returns>A worker that performs no computation.</returns>
protected override AsyncWorkerBase CreateWorker(Action<string> progressReporter)
{
return new NoopWorker(this, AddRuntimeMessage);
}

/// <summary>
/// Minimal worker for badge test components. It gathers no input, performs no work,
/// and sets only a lightweight status message so the async pipeline completes.
/// </summary>
private sealed class NoopWorker : AsyncWorkerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="NoopWorker"/> class.
/// </summary>
/// <param name="parent">Component instance.</param>
/// <param name="addRuntimeMessage">Delegate to add runtime messages.</param>
public NoopWorker(GH_Component parent, Action<GH_RuntimeMessageLevel, string> addRuntimeMessage)
: base(parent, addRuntimeMessage)
{
}

/// <inheritdoc />
public override void GatherInput(IGH_DataAccess DA, out int dataCount)
{
dataCount = 0;
}

/// <inheritdoc />
public override Task DoWorkAsync(CancellationToken token)
{
return Task.CompletedTask;
}

/// <inheritdoc />
public override void SetOutput(IGH_DataAccess DA, out string message)
{
message = "Ready";
}
}

private class ThreeBadgesAttributes : ComponentBadgesAttributes
{
private const float S = 16f;

private static void DrawBlue(Graphics g, float x, float y)
{
using var bg = new SolidBrush(Color.FromArgb(52, 152, 219));
using var pen = new Pen(Color.White, 1.5f);
var rect = new RectangleF(x, y, S, S);
g.FillEllipse(bg, rect);
g.DrawEllipse(pen, rect);
}

private static void DrawPurple(Graphics g, float x, float y)
{
using var bg = new SolidBrush(Color.FromArgb(155, 89, 182));
using var pen = new Pen(Color.White, 1.5f);
var rect = new RectangleF(x, y, S, S);
g.FillEllipse(bg, rect);
g.DrawEllipse(pen, rect);
}

private static void DrawTeal(Graphics g, float x, float y)
{
using var bg = new SolidBrush(Color.FromArgb(26, 188, 156));
using var pen = new Pen(Color.White, 1.5f);
var rect = new RectangleF(x, y, S, S);
g.FillEllipse(bg, rect);
g.DrawEllipse(pen, rect);
}

public ThreeBadgesAttributes(AIProviderComponentBase owner) : base(owner) { }

protected override IEnumerable<(Action<Graphics, float, float> draw, string label)> GetAdditionalBadges()
{
yield return (DrawBlue, "Sample badge 1");
yield return (DrawPurple, "Sample badge 2");
yield return (DrawTeal, "Sample badge 3");
}
}
}
}
Loading
Loading