Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8ca083e
refactor[SCRUM-30]: Update canvas services and frontend structure
ChaturnaK Mar 22, 2026
7a2814c
feat[SCRUM-30]: Integrate CanvasMarkdown component for enhanced conte…
ChaturnaK Mar 22, 2026
42a5cfb
feat[SCRUM-30]: Implement canvas node management enhancements
ChaturnaK Mar 24, 2026
b35fcb5
feat[SCRUM-30]: Enhance CanvasGraphResponse and UI components
ChaturnaK Mar 24, 2026
5473a41
feat[SCRUM-***]: Implemented Core Features (Absolute Cinema)
ckasturiratna Mar 27, 2026
0815a61
Expand chat replies and focus branches with fitView
ckasturiratna Mar 27, 2026
f0eae24
feat [SCRUM-60]: implement Sub-Node creation with context snapshot an…
Sunny-Hasho Apr 6, 2026
9cbf111
feat [SCRUM-21]: allow sub-node to inherit context snapshot for indep…
Sunny-Hasho Apr 6, 2026
7a4e07f
feat [SCRUM-88]: enable draggable nodes and persist layout positions
Sunny-Hasho Apr 6, 2026
f101eff
feat [SCRUM-90]: implement node hover tooltips with fetch-on-hover logic
Sunny-Hasho Apr 6, 2026
7d93d82
feat [US-302]: remove redundant Create Sub-Node feature
Sunny-Hasho Apr 6, 2026
62adf2d
chore: new frontend connector
ChaturnaK Apr 13, 2026
09e9d38
feat [SCRUM-177]: add inline move/copy branch creation for selected m…
Sunny-Hasho Apr 18, 2026
c94f1ae
chore [SCRUM-177]: stop tracking .playwright-cli and output artifacts
Sunny-Hasho Apr 18, 2026
fa37bb9
fix [SCRUM-177]: align SettingsPage tests with current UI copy
Sunny-Hasho Apr 18, 2026
8baa5f8
feat [SCRUM-177]: improve chat send UX with optimistic thread and com…
Sunny-Hasho Apr 18, 2026
fd42bab
fix [SCRUM-177]: show node resize handles without selecting node first
Sunny-Hasho Apr 18, 2026
3e170d4
Merge branch 'develop' into feature/SCRUM-177-inline-branch-creation-new
Sunny-Hasho Apr 18, 2026
8608beb
Merge pull request #72 from Forklie/feature/SCRUM-177-inline-branch-c…
Dinuka-Nonis Apr 18, 2026
7ce9b48
feat [SCRUM-28]: add html2pdf.js runtime and typings for PDF export
Heshan1478 Apr 18, 2026
9e68267
feat [SCRUM-28]: extend node data types to support export mode, selec…
Heshan1478 Apr 18, 2026
7bea682
feat [SCRUM-28]: enable AI summary mapping for chat nodes in export w…
Heshan1478 Apr 18, 2026
ae8c1ef
feat [SCRUM-28]: add node-level menu with export options and selectio…
Heshan1478 Apr 18, 2026
22e1181
feat [SCRUM-28]: implement selection mode and cross-node export workf…
Heshan1478 Apr 18, 2026
fb352e8
feat [SCRUM-28]: implement selection mode and cross-node export workf…
Heshan1478 Apr 18, 2026
5480819
feat [SCRUM-28]: implement selection mode and cross-node export workf…
Heshan1478 Apr 18, 2026
bc5a38d
fix [SCRUM-28]: resolve html2pdf.js Vite build error
Heshan1478 Apr 18, 2026
1762734
fix [SCRUM-28]: update vite.config
Heshan1478 Apr 18, 2026
70867f6
refactor [SCRUM-28]: replace html2pdf with html2canvas and jsPDF for …
Heshan1478 Apr 18, 2026
3468165
chore [SCRUM-28]: clean vite config by removing html2pdf-specific opt…
Heshan1478 Apr 18, 2026
bddc0e7
fix [SCRUM-28]: replace html2pdf.js with jspdf and html2canvas
Heshan1478 Apr 18, 2026
2409fcf
fix [SCRUM-28]: rewrite PDF export using jsPDF text mode, remove html…
Heshan1478 Apr 18, 2026
f73dfd1
refactor [SCRUM-28]: update package.json
Heshan1478 Apr 18, 2026
cb5b9b3
feat [SCRUM-28]: add LM Studio provider support, including configurat…
ChaturnaK Apr 19, 2026
2ff053a
Merge pull request #73 from Forklie/feature/scrum-28-export-pdf
Dinuka-Nonis Apr 20, 2026
b812e42
test [SCRUM-185]: finalize sprint 4 QA integration and regression cov…
Sunny-Hasho Apr 20, 2026
44c06b4
test [SCRUM-185]: improve headed UI preview flows
Sunny-Hasho Apr 20, 2026
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
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,18 @@ Llm__Providers__OpenAI__Model=gpt-5.1
Llm__Providers__Claude__Model=claude-3-5-haiku-latest
Llm__Providers__Claude__Version=2023-06-01
Llm__Providers__Gemini__Model=gemini-3.1-flash-lite
# LM Studio does not require auth by default; a harmless placeholder such as
# "lmstudio" is sufficient for the saved provider key when auth is off.
# If the backend runs in Docker, point this at a host-reachable LM Studio server,
# for example http://host.docker.internal:1234/v1.
Llm__Providers__LmStudio__BaseUrl=http://localhost:1234/v1
Llm__Providers__LmStudio__Model=local-model

# Built-in guest preview keys used once per logged-out browser session.
# These should be low-quota or dedicated guest credentials.
Llm__GuestPreview__OpenAI__ApiKey=
Llm__GuestPreview__OpenAI__Model=gpt-5.2
Llm__GuestPreview__Claude__ApiKey=
Llm__GuestPreview__Claude__Model=claude-sonnet-4-20250514
Llm__GuestPreview__Gemini__ApiKey=
Llm__GuestPreview__Gemini__Model=gemini-2.5-flash
Binary file modified .gitignore
Binary file not shown.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ docker compose up --build
Coliee/
├── .github/workflows/ CI/CD pipelines (GitHub Actions)
├── backend/Coliee.API/ .NET 8 Web API
├── frontend/ React frontend (Vite + Nginx)
├── frontend/ Unified React frontend (Vite + Nginx)
├── docker-compose.yml Multi-service orchestration
├── init.sql MySQL schema initialisation
├── .env.example Environment variable template
Expand Down
6 changes: 6 additions & 0 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ Go to your repo → **Settings** → **Secrets and variables** → **Actions** a
| `SMTP_PORT` | `587` |
| `SMTP_USER` | Your Gmail address |
| `SMTP_PASSWORD` | [Google App Password](https://myaccount.google.com/apppasswords) |
| `Llm__GuestPreview__OpenAI__ApiKey` | Dedicated one-time guest preview key for OpenAI |
| `Llm__GuestPreview__OpenAI__Model` | Default built-in OpenAI guest model |
| `Llm__GuestPreview__Claude__ApiKey` | Dedicated one-time guest preview key for Claude |
| `Llm__GuestPreview__Claude__Model` | Default built-in Claude guest model |
| `Llm__GuestPreview__Gemini__ApiKey` | Dedicated one-time guest preview key for Gemini |
| `Llm__GuestPreview__Gemini__Model` | Default built-in Gemini guest model |

---

Expand Down
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
- � **Note Generation** — synthesise notes from any combination of nodes
- � **Export** — download notes as PDF or plain text
- 🔒 **Secure Authentication** — JWT-based auth with BCrypt password hashing
- 🔑 **BYOK Provider Settings** — save and manage personal OpenAI, Claude, and Gemini keys
- 💬 **Main-Node Chat** — minimal chat surface on `/home` using the selected BYOK provider
- 🔑 **BYOK Provider Settings** — save and manage personal OpenAI, Claude, Gemini, and LM Studio keys
- 💬 **Canvas Workspace** — the primary frontend experience now lives in the unified canvas workspace on `/`

---

Expand All @@ -27,7 +27,7 @@
| Frontend | React.js (functional components + hooks), React Flow |
| Backend | ASP.NET Web API (.NET 8), ADO.NET |
| Database | MySQL 8.0 hosted on Supabase |
| AI | OpenAI API (GPT-4 / GPT-3.5-turbo) |
| AI | OpenAI, Anthropic Claude, and Google Gemini APIs |
| Proxy | Nginx reverse proxy |
| DevOps | Docker, GitHub Actions CI/CD |
| Hosting | Azure App Service (API) + Azure Static Web Apps (frontend) |
Expand Down Expand Up @@ -56,7 +56,8 @@ Coliee/
- [Docker](https://docs.docker.com/get-docker/) & Docker Compose
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) *(for backend development outside Docker)*
- [Node.js 18+](https://nodejs.org/) *(for frontend development outside Docker)*
- A provider API key for OpenAI, Claude, or Gemini if you want to use the BYOK chat flow
- A provider API key for OpenAI, Claude, Gemini, or LM Studio if you want to use the BYOK chat flow
- Optional built-in guest preview API keys for OpenAI, Claude, and Gemini if you want logged-out users to get one real AI action before sign-in

### 1. Clone the repository

Expand All @@ -81,6 +82,18 @@ MYSQL_ROOT_PASSWORD=your_root_password

JWT_KEY=your_jwt_secret
Llm__EncryptionKey=replace-with-base64-32-byte-key
Llm__GuestPreview__OpenAI__ApiKey=
Llm__GuestPreview__OpenAI__Model=gpt-5.2
Llm__GuestPreview__Claude__ApiKey=
Llm__GuestPreview__Claude__Model=claude-sonnet-4-20250514
Llm__GuestPreview__Gemini__ApiKey=
Llm__GuestPreview__Gemini__Model=gemini-2.5-flash
# LM Studio does not require auth by default; a placeholder such as
# "lmstudio" is enough for the saved provider key when auth is off.
# If the backend runs in Docker, point LM Studio at a host-reachable URL such as
# http://host.docker.internal:1234/v1.
Llm__Providers__LmStudio__BaseUrl=http://localhost:1234/v1
Llm__Providers__LmStudio__Model=local-model

# Backend API URL (used by frontend)
VITE_API_URL=http://localhost:5000
Expand Down Expand Up @@ -162,7 +175,7 @@ The backend follows a **layered structure**:
Controller → Service → Data Access (ADO.NET) → MySQL
```

All LLM providers are abstracted behind an interface, and the current BYOK flow supports OpenAI, Claude, and Gemini using user-managed keys stored encrypted at rest.
All LLM providers are abstracted behind an interface, and the current BYOK flow supports OpenAI, Claude, Gemini, and LM Studio using user-managed keys stored encrypted at rest. Logged-out guest canvas actions can also use dedicated built-in preview keys for a single real AI action before sign-in is required.

---

Expand Down
10 changes: 9 additions & 1 deletion backend/Coliee.API.Tests/AuthControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Coliee.API.DTOs;
using Coliee.API.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Coliee.API.Tests;
Expand Down Expand Up @@ -57,7 +58,14 @@ public async Task ResendPasswordResetOtp_ReturnsRateLimitPayload_OnBlock()
}

private static AuthController CreateController(IPasswordResetOtpService passwordResetOtpService) =>
new(null!, null!, passwordResetOtpService, new StubEmailVerificationOtpService(), new StubPasswordPolicyService(), new StubGoogleAuthService());
new(
null!,
null!,
passwordResetOtpService,
new StubEmailVerificationOtpService(),
new StubPasswordPolicyService(),
new StubGoogleAuthService(),
NullLogger<AuthController>.Instance);
}

internal sealed class StubPasswordResetOtpService : IPasswordResetOtpService
Expand Down
4 changes: 3 additions & 1 deletion backend/Coliee.API.Tests/Controllers/AuthControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Coliee.API.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Coliee.API.Tests.Controllers;
Expand Down Expand Up @@ -49,7 +50,7 @@
Assert.Equal("user@example.com", payload.Email);
Assert.Equal(2, payload.ResendAttemptsRemaining);
Assert.Equal(1, repo.ResetFailedLoginCallCount);
Assert.Equal(0, repo.StoredRefreshTokens.Count);

Check warning on line 53 in backend/Coliee.API.Tests/Controllers/AuthControllerTests.cs

View workflow job for this annotation

GitHub Actions / build_backend

Do not use Assert.Equal() to check for collection size. Use Assert.Empty instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013)

Check warning on line 53 in backend/Coliee.API.Tests/Controllers/AuthControllerTests.cs

View workflow job for this annotation

GitHub Actions / build_backend

Do not use Assert.Equal() to check for collection size. Use Assert.Empty instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013)
}

[Fact]
Expand All @@ -62,7 +63,7 @@

var unauthorizedResult = Assert.IsType<UnauthorizedObjectResult>(result);
Assert.Equal(401, unauthorizedResult.StatusCode);
Assert.Equal(0, repo.StoredRefreshTokens.Count);

Check warning on line 66 in backend/Coliee.API.Tests/Controllers/AuthControllerTests.cs

View workflow job for this annotation

GitHub Actions / build_backend

Do not use Assert.Equal() to check for collection size. Use Assert.Empty instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013)

Check warning on line 66 in backend/Coliee.API.Tests/Controllers/AuthControllerTests.cs

View workflow job for this annotation

GitHub Actions / build_backend

Do not use Assert.Equal() to check for collection size. Use Assert.Empty instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013)
Assert.Equal(0, repo.CreateGoogleUserCallCount);
}

Expand All @@ -79,7 +80,7 @@

var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(503, objectResult.StatusCode);
Assert.Equal(0, repo.StoredRefreshTokens.Count);

Check warning on line 83 in backend/Coliee.API.Tests/Controllers/AuthControllerTests.cs

View workflow job for this annotation

GitHub Actions / build_backend

Do not use Assert.Equal() to check for collection size. Use Assert.Empty instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013)

Check warning on line 83 in backend/Coliee.API.Tests/Controllers/AuthControllerTests.cs

View workflow job for this annotation

GitHub Actions / build_backend

Do not use Assert.Equal() to check for collection size. Use Assert.Empty instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013)
Assert.Equal(0, repo.CreateGoogleUserCallCount);
}

Expand Down Expand Up @@ -154,7 +155,7 @@

var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(429, objectResult.StatusCode);
Assert.Equal(0, repo.StoredRefreshTokens.Count);

Check warning on line 158 in backend/Coliee.API.Tests/Controllers/AuthControllerTests.cs

View workflow job for this annotation

GitHub Actions / build_backend

Do not use Assert.Equal() to check for collection size. Use Assert.Empty instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013)

Check warning on line 158 in backend/Coliee.API.Tests/Controllers/AuthControllerTests.cs

View workflow job for this annotation

GitHub Actions / build_backend

Do not use Assert.Equal() to check for collection size. Use Assert.Empty instead. (https://xunit.net/xunit.analyzers/rules/xUnit2013)
Assert.Equal(0, repo.ResetFailedLoginCallCount);
}

Expand Down Expand Up @@ -220,7 +221,8 @@
new StubPasswordResetOtpService(),
emailVerificationOtpService ?? new StubEmailVerificationOtpService(),
new StubPasswordPolicyService(),
googleAuthService);
googleAuthService,
NullLogger<AuthController>.Instance);
}

private static TokenService CreateTokenService()
Expand Down
91 changes: 70 additions & 21 deletions backend/Coliee.API.Tests/Controllers/CanvasesControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task GetCanvases_ReturnsOrderedCanvasSummaries()
new CanvasSummaryResponse { Id = 1, Title = "Older" }
]
};
var controller = CreateController(service, new FakeCanvasGraphService());
var controller = CreateController(new FakeCanvasCreationService(), service, new FakeCanvasGraphService());

var result = await controller.GetCanvases();

Expand All @@ -34,32 +34,57 @@ public async Task GetCanvases_ReturnsOrderedCanvasSummaries()
public async Task CreateCanvas_ReturnsCreatedStatus()
{
var service = new FakeCanvasService
{ };
var creationService = new FakeCanvasCreationService
{
CreateResult = new CanvasSummaryResponse
{
Id = 10,
Title = "Launch checklist"
Title = "Launch Checklist"
}
};
var controller = CreateController(service, new FakeCanvasGraphService());
var controller = CreateController(creationService, service, new FakeCanvasGraphService());

var result = await controller.CreateCanvas(new CreateCanvasRequest { FirstPrompt = "Launch checklist" });

var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(201, objectResult.StatusCode);
var payload = Assert.IsType<CanvasSummaryResponse>(objectResult.Value);
Assert.Equal(10, payload.Id);
Assert.Equal("Launch checklist", payload.Title);
Assert.Equal("Launch Checklist", payload.Title);
}

[Fact]
public async Task CreateCanvas_PassesGenerationModeToCreationService()
{
var creationService = new FakeCanvasCreationService
{
CreateResult = new CanvasSummaryResponse
{
Id = 11,
Title = "Launch Checklist"
}
};
var controller = CreateController(creationService, new FakeCanvasService(), new FakeCanvasGraphService());

await controller.CreateCanvas(new CreateCanvasRequest
{
FirstPrompt = "Launch checklist",
GenerationMode = "rich"
});

Assert.Equal("rich", creationService.LastGenerationMode);
}

[Fact]
public async Task CreateCanvas_WhenServiceRejectsPrompt_ReturnsBadRequest()
{
var service = new FakeCanvasService
var service = new FakeCanvasService();
var creationService = new FakeCanvasCreationService
{
ExceptionToThrow = new CanvasOperationException("A first prompt is required to create a canvas.", 400)
};
var controller = CreateController(service, new FakeCanvasGraphService());
var controller = CreateController(creationService, service, new FakeCanvasGraphService());

var result = await controller.CreateCanvas(new CreateCanvasRequest { FirstPrompt = " " });

Expand All @@ -78,7 +103,7 @@ public async Task RenameCanvas_ReturnsUpdatedCanvas()
Title = "Renamed canvas"
}
};
var controller = CreateController(service, new FakeCanvasGraphService());
var controller = CreateController(new FakeCanvasCreationService(), service, new FakeCanvasGraphService());

var result = await controller.RenameCanvas(10, new RenameCanvasRequest { Title = "Renamed canvas" });

Expand All @@ -94,7 +119,7 @@ public async Task RenameCanvas_WhenServiceRejectsTitle_ReturnsBadRequest()
{
ExceptionToThrow = new CanvasOperationException("A canvas title is required.", 400)
};
var controller = CreateController(service, new FakeCanvasGraphService());
var controller = CreateController(new FakeCanvasCreationService(), service, new FakeCanvasGraphService());

var result = await controller.RenameCanvas(10, new RenameCanvasRequest { Title = " " });

Expand All @@ -105,7 +130,7 @@ public async Task RenameCanvas_WhenServiceRejectsTitle_ReturnsBadRequest()
[Fact]
public async Task DeleteCanvas_ReturnsNoContent()
{
var controller = CreateController(new FakeCanvasService(), new FakeCanvasGraphService());
var controller = CreateController(new FakeCanvasCreationService(), new FakeCanvasService(), new FakeCanvasGraphService());

var result = await controller.DeleteCanvas(10);

Expand All @@ -119,7 +144,7 @@ public async Task DeleteCanvas_WhenForbidden_ReturnsForbidden()
{
DeleteExceptionToThrow = new CanvasOperationException("You do not have access to that canvas.", 403)
};
var controller = CreateController(service, new FakeCanvasGraphService());
var controller = CreateController(new FakeCanvasCreationService(), service, new FakeCanvasGraphService());

var result = await controller.DeleteCanvas(10);

Expand All @@ -134,7 +159,7 @@ public async Task DeleteCanvas_WhenMissing_ReturnsNotFound()
{
DeleteExceptionToThrow = new CanvasOperationException("Canvas not found.", 404)
};
var controller = CreateController(service, new FakeCanvasGraphService());
var controller = CreateController(new FakeCanvasCreationService(), service, new FakeCanvasGraphService());

var result = await controller.DeleteCanvas(999);

Expand All @@ -158,7 +183,7 @@ public async Task GetCanvasGraph_ReturnsGraphPayload()
]
}
};
var controller = CreateController(new FakeCanvasService(), graphService);
var controller = CreateController(new FakeCanvasCreationService(), new FakeCanvasService(), graphService);

var result = await controller.GetCanvasGraph(10);

Expand All @@ -175,17 +200,32 @@ public async Task ExpandCanvasNode_WhenProviderMissing_ReturnsBadRequestWithCode
{
LlmExceptionToThrow = new LlmChatException("Missing provider", LlmProviderErrorCodes.MissingKey, 400)
};
var controller = CreateController(new FakeCanvasService(), graphService);
var controller = CreateController(new FakeCanvasCreationService(), new FakeCanvasService(), graphService);

var result = await controller.ExpandCanvasNode(10, 42, new ExpandCanvasNodeRequest { Prompt = "Hello" });

var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(400, objectResult.StatusCode);
}

private static CanvasesController CreateController(ICanvasService service, ICanvasGraphService graphService, int userId = 1)
[Fact]
public async Task ExpandCanvasNode_PassesGenerationModeToGraphService()
{
var graphService = new FakeCanvasGraphService();
var controller = CreateController(new FakeCanvasCreationService(), new FakeCanvasService(), graphService);

await controller.ExpandCanvasNode(10, 42, new ExpandCanvasNodeRequest
{
Prompt = "Hello",
GenerationMode = "rich"
});

Assert.Equal("rich", graphService.LastGenerationMode);
}

private static CanvasesController CreateController(ICanvasCreationService creationService, ICanvasService service, ICanvasGraphService graphService, int userId = 1)
{
var controller = new CanvasesController(service, graphService);
var controller = new CanvasesController(creationService, service, graphService);
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
Expand All @@ -202,21 +242,28 @@ private static CanvasesController CreateController(ICanvasService service, ICanv
return controller;
}

private sealed class FakeCanvasService : ICanvasService
private sealed class FakeCanvasCreationService : ICanvasCreationService
{
public IReadOnlyList<CanvasSummaryResponse> Items { get; set; } = [];
public CanvasSummaryResponse CreateResult { get; set; } = new();
public CanvasSummaryResponse RenameResult { get; set; } = new();
public Exception? ExceptionToThrow { get; set; }
public Exception? DeleteExceptionToThrow { get; set; }
public string? LastGenerationMode { get; private set; }

public Task<CanvasSummaryResponse> CreateCanvasAsync(int userId, string firstPrompt)
public Task<CanvasSummaryResponse> CreateCanvasAsync(int userId, string firstPrompt, string? generationMode)
{
if (ExceptionToThrow != null)
throw ExceptionToThrow;

LastGenerationMode = generationMode;
return Task.FromResult(CreateResult);
}
}

private sealed class FakeCanvasService : ICanvasService
{
public IReadOnlyList<CanvasSummaryResponse> Items { get; set; } = [];
public CanvasSummaryResponse RenameResult { get; set; } = new();
public Exception? ExceptionToThrow { get; set; }
public Exception? DeleteExceptionToThrow { get; set; }

public Task<IReadOnlyList<CanvasSummaryResponse>> GetCanvasesAsync(int userId)
{
Expand Down Expand Up @@ -246,6 +293,7 @@ private sealed class FakeCanvasGraphService : ICanvasGraphService
public CanvasGraphResponse ExpandResult { get; set; } = new();
public Exception? ExceptionToThrow { get; set; }
public Exception? LlmExceptionToThrow { get; set; }
public string? LastGenerationMode { get; private set; }

public Task<CanvasGraphResponse> GetCanvasGraphAsync(int userId, int canvasId)
{
Expand All @@ -255,14 +303,15 @@ public Task<CanvasGraphResponse> GetCanvasGraphAsync(int userId, int canvasId)
return Task.FromResult(GraphResult);
}

public Task<CanvasGraphResponse> ExpandCanvasNodeAsync(int userId, int canvasId, int nodeId, string prompt, string? providerOverride)
public Task<CanvasGraphResponse> ExpandCanvasNodeAsync(int userId, int canvasId, int nodeId, string prompt, string? providerOverride, string? generationMode)
{
if (ExceptionToThrow != null)
throw ExceptionToThrow;

if (LlmExceptionToThrow != null)
throw LlmExceptionToThrow;

LastGenerationMode = generationMode;
return Task.FromResult(ExpandResult);
}
}
Expand Down
Loading
Loading