← mindattic.com

ThinkTank

.NET 10 MAUI + Blazor desktop app for multi-LLM conversations across 11 providers.

Think Tank

Stop arguing with one AI. Convene a roundtable.

Think Tank turns the world's frontier LLMs into a panel of advisors that debate, refine, and decide — together. Pick your participants from 11 providers (OpenAI, Anthropic, Google, DeepSeek, Mistral, xAI, Groq, Together AI, OpenRouter, Fireworks, and Cohere), assign each one a personality, drop a topic into the room, and watch them think out loud. Inject your own messages mid-discussion to steer the conversation. Call a vote when they go in circles. Run multiple debates in parallel tabs.

Whether you're stress-testing a product decision, exploring ethical edge cases, drafting strategy, or just curious whether Claude and ChatGPT actually disagree — Think Tank gives you the room, the seats, and the gavel.

Why it's different:


A .NET 10 ASP.NET Core Blazor Server application that orchestrates multi-participant AI discussions across every major LLM provider. Create conversation panels where ChatGPT, Claude, Gemini, and DeepSeek debate topics with customizable personalities — and inject your own messages to steer the conversation in real time.


Table of Contents #


Overview #

Think Tank lets you pit multiple AI models against each other in structured conversations. Each participant has a customizable personality, and the app manages turn-taking, history sharing, and conversation persistence automatically. You can run multiple conversations in parallel using tabs, watch the discussion unfold, and jump in with your own messages at any time.

Built with:


Features #

Multi-Provider AI Conversations #

Conversation Tabs #

Personality System #

User Chat Injection #

Random Topic Generation #

Theme System #

Provider Connectivity #

Vote-Driven Decisions #

Perspective Tracking #

Diagnostics #


Architecture #

+--------------------------------------------------------------+
|                         Browser                              |
|   Blazor Server components (Home / Chat / Settings / ...)    |
+------------------------------|-------------------------------+
                               | SignalR (interactive server)
+------------------------------v-------------------------------+
|                ThinkTank.Blazor (ASP.NET Core)               |
|   Program.cs wires DI: Legion, Vault, services, voting       |
+------------------------------|-------------------------------+
                               |
        +----------------------+----------------------+
        v                      v                      v
+---------------+    +-------------------+    +----------------+
| ThinkTank.    |    | ThinkTank.Core    |    | ThinkTank.     |
|   Shared      |    |   (services +     |    |   Blazor       |
| (Razor lib +  |    |    models)        |    |   (host)       |
|   wwwroot)    |    +---------+---------+    +----------------+
+---------------+              |
                               v
                  +--------------------------+
                  |    MindAttic.Legion      |
                  |   (LegionClient +        |
                  |    LLMVotingService)     |
                  +-------------+------------+
                                |
            +-------------------+-------------------+
            v                   v                   v
        OpenAI            Anthropic            ... 9 more
       (ChatGPT)            (Claude)             providers

All services are registered as singletons in ThinkTank.Blazor/Program.cs for shared global state across user circuits.


Getting Started #

Prerequisites #

Build & Run #

# Restore dependencies
dotnet restore

# Run the Blazor Server app (default URL: https://localhost:5001)
dotnet run --project ThinkTank.Blazor

# Or specify a port
dotnet run --project ThinkTank.Blazor --urls http://localhost:5100

Open the URL in any modern browser. The app uses SignalR for interactive components, so a stable connection is required (it reconnects automatically on transient drops).

First Launch #

  1. Navigate to Settings > Providers
  2. Enter your API key(s) in the JSON editor for each provider
  3. Click model chips to set your preferred model
  4. Go to Conversations (Think Tank)
  5. Enter a topic, select participants, and click Start

Testing #

The project ships with two complementary test suites.

Unit / component tests — NUnit + bUnit covering services, model parsing, Razor component rendering, the cloud-credential overlay, voting marker regexes, and the ChatParticipantVoterProfile mapping:

dotnet test ThinkTank.UnitTests/ThinkTank.UnitTests.csproj

End-to-end UI tests — Cypress, 4 specs (navigation, settings, chat, vote-dialog) covering top-level affordances. Run a dev server in one terminal, then the suite in another:

# Terminal 1 — start the Blazor app on http://localhost:5100
dotnet run --project ThinkTank.Blazor --urls http://localhost:5100

# Terminal 2 — install once, then run the suite
npm install
npx cypress run

# Or open the interactive runner
npx cypress open

Override CYPRESS_BASE_URL if the server is on a different host/port. The suite assumes a freshly seeded settings store and does not fire live LLM calls — LLM dispatch through MindAttic.Legion is covered by the unit suite.


Configuration #

Provider Auth #

Each provider's configuration is stored as a JSON object:

{
  "type": "bearer",
  "apiKey": "sk-your-key-here",
  "model": "gpt-4o",
  "maxTokens": 2048
}
Field Description
type Auth type: "bearer" (OpenAI-compatible), "anthropic" (Claude), or "google" (Gemini)
apiKey Your API key for the provider
model Model identifier to use for API calls
maxTokens Maximum output tokens per response (default: 2048)

Models surfaced as selectable chips in Settings are sourced from MindAttic.Legion's LlmProviderCatalog. As Legion's catalog grows, the Think Tank UI picks up new providers and models automatically.

Cloud-Native Credentials (MindAttic.Vault) #

For deployments where API keys live outside Settings.json — Azure App Service Application Settings, Key Vault references, or shared dev user-secrets — Think Tank reads each provider's apiKey from IConfiguration at the path:

MindAttic:Vault:LLM:<providerId>:apiKey

Configuration sources are layered in ThinkTank.Blazor/Program.cs (later wins): appsettings.json%APPDATA%\MindAttic\LLM\providers.json (via AddMindAtticVaultFiles) → project user-secrets → shared user-secrets (mindattic-vault-shared) → environment variables (App Service Application Settings + Key Vault refs).

Precedence within GetKeyForProvider:

  1. Explicit per-call override (e.g. a participant's AuthOverrideJson).
  2. The on-disk apiKey in Settings.json if non-empty — explicit local edits win.
  3. The runtime override resolved from any of the configuration sources above.

Cloud-resolved keys live in a runtime-only side map (RuntimeApiKeyOverrides) and are never written back to Settings.json or the shared %APPDATA%\MindAttic\LLM\providers.json store. A cloud deployment can therefore run with a completely empty disk projection.

Personalities #

Personalities are markdown templates that define how each AI participant behaves in conversation. The personality markdown is sent as the system prompt to the provider's API.

Appearance #

All visual customization is in Settings > Appearance:

Setting Range Default Description
Theme 18 options dark Color scheme for the entire app
Control Height 28-60px 40px Height of buttons, inputs, and interactive elements
Gutter 0-30px 10px Spacing between UI elements
Border Radius 0-24px 10px Roundness of corners

Voting #

The Call Vote button (visible in the conversation header once a tab has 2+ participants and the conversation has started) polls every participant on a question and injects the aggregated decision back into the shared history so subsequent turns see it. The vote itself runs through MindAttic.Legion.LLMVotingService.

Vote types:

Type Question Options Quorum
Consensus "Have we reached consensus?" Yes / No Simple majority
Free-form "What is our conclusion?" (open-ended) Plurality
Direction "What direction next?" Comma-separated user-supplied options Simple majority

The result lands in the conversation as a synthetic turn formatted:

[VOTE] Question: <question>
Decision: <consensus> (<percentage> agreement)
Summary: <narrativeSummary>

Auto-vote: the system prompt for every participant ends with an instruction telling them they may emit [REQUEST_VOTE: question] anywhere in their response if they detect a stalemate. The marker is stripped from the visible response and triggers an immediate consensus vote on the requested question. Implementation lives in Chat.razor (search for VoteRequestInstruction and RunParticipantVoteAsync); regex coverage is in VoteMarkerTests.cs.

The mapping from ChatParticipant to Legion's VoterProfile (preserving each participant's persona and per-participant API/model overrides) is implemented in VotingService.MapToVoterProfiles.


How It Works #

Conversation Flow #

User enters topic + selects participants + clicks Start
         |
         v
    +---------+
    | Round N  |<------------------------------+
    +----+-----+                               |
         |                                     |
         v                                     |
   +-----------+    yes    +----------+        |
   |User typing?+--------->|  Wait... |        |
   +-----+-----+          +----+-----+        |
     no  |                      | (user sends  |
         |                      |  or clears)  |
         |<---------------------+              |
         v                                     |
   For each participant:                       |
    +-- Set as current speaker (typing dots)   |
    +-- Call LegionClient.SendAsync(...)        |
    +-- Add response to messages               |
    +-- Save to chat.json + perspective.md     |
    +-- Wait 400ms                             |
         |                                     |
         v                                     |
   +-------------+   no                        |
   |Stop pressed? +--------------------------->/
   +------+------+
      yes |
          v
     Conversation ends

User Chat Injection #

  1. Focus the message input field at the bottom of the chat
  2. The conversation pauses (overlay appears: "Conversation Paused")
  3. Type your message and press Enter or click Send
  4. Your message appears in the chat as "You" with a user avatar
  5. The message is added to shared history for the next round
  6. The conversation resumes automatically

Title Generation #

After the first round completes, the app generates a conversation title in the background:

  1. Each participant is asked to suggest a concise title (max 5 words)
  2. The first participant then picks the best title from all suggestions
  3. The tab title updates automatically

Project Structure #

ThinkTank/
+-- ThinkTank.Blazor/                  # ASP.NET Core host
|   +-- Components/
|   |   +-- App.razor                  # Root component
|   |   +-- Routes.razor
|   |   +-- _Imports.razor
|   +-- Program.cs                     # DI registration (Legion, Vault, services)
|   +-- Properties/launchSettings.json
|   +-- ThinkTank.Blazor.csproj        # Web SDK, net10.0
+-- ThinkTank.Shared/                  # Razor class library (UI)
|   +-- Components/
|   |   +-- Layout/
|   |   |   +-- MainLayout.razor       # App shell (NavMenu + content)
|   |   |   +-- NavMenu.razor          # Top nav bar (Home, Conversations, Settings)
|   |   +-- Pages/
|   |   |   +-- Home.razor             # Landing page
|   |   |   +-- Chat.razor             # Main think tank UI
|   |   |   +-- Settings.razor         # Provider auth + personality editor
|   |   |   +-- SettingsAppearance.razor
|   |   |   +-- NotFound.razor
|   |   +-- Shared/
|   |       +-- ConfirmationDialog.razor
|   +-- wwwroot/                       # Global CSS, themes, JS interop, Bootstrap
|   |   +-- app.css                    # All 18 theme definitions
|   |   +-- theme.js                   # JS interop for theme/control sizing
|   |   +-- lib/bootstrap/
|   +-- ThinkTank.Shared.csproj        # Razor SDK, net10.0
+-- ThinkTank.Core/                    # Services + models (no UI)
|   +-- Services/
|   |   +-- ThinkTankService.cs        # Core AI orchestration via Legion
|   |   +-- VotingService.cs           # ChatParticipant -> VoterProfile mapping
|   |   +-- SettingsService.cs         # Persistence + provider auth
|   |   +-- SettingsServiceVaultOverlay.cs
|   |   +-- AppearanceService.cs       # Theme management
|   |   +-- ChatConversationsService.cs
|   |   +-- ChatLogService.cs
|   |   +-- HumanNameService.cs        # Random name generation
|   |   +-- NameGeneratorService.cs    # AI-powered name generation
|   +-- Models/
|   |   +-- ChatModels.cs
|   |   +-- ChatLogModels.cs
|   |   +-- LlmModels.cs
|   |   +-- PersistenceModels.cs
|   |   +-- ProviderAuthConfig.cs
|   |   +-- AppearanceMode.cs
|   +-- ThinkTank.Core.csproj          # References MindAttic.Legion + Vault
+-- ThinkTank.UnitTests/               # NUnit + bUnit
+-- cypress/                           # Cypress e2e specs
|   +-- e2e/{chat,navigation,settings,vote-dialog}.cy.js
+-- ThinkTank.slnx

Services #

ThinkTankService #

The core orchestration engine. Every LLM call is delegated to MindAttic.Legion.LegionClient; Think Tank's job is to assemble the prompt and history in Legion's canonical shape.

Key responsibilities:

VotingService #

Maps ChatParticipant[] (with per-participant API/model overrides) into Legion's VoterProfile[] and delegates to LLMVotingService.VoteWithProfilesAsync. Returns a VoteResult shaped for injection back into the shared conversation history.

SettingsService #

Handles all persistence to %LOCALAPPDATA%\MindAttic\ThinkTank\Settings.json.

Manages:

Migrations:

AppearanceService #

Manages the 18-theme system and visual customization. Changes are applied via CSS variable updates through JS interop and persisted via SettingsService.

ChatConversationsService #

Manages the conversation tab system. Uses ObservableCollection<ChatConversation> for reactive UI updates. Handles tab creation, switching, closing, and persistence.

ChatLogService / ChatStorage #

ChatLogService: In-memory log of chat events with a Changed event for UI updates.

ChatStorage (static): File-based persistence per conversation:


Theming #

The app uses CSS custom properties (variables) defined per theme in ThinkTank.Shared/wwwroot/app.css. Each theme is a html[data-theme="..."] selector block that overrides the root variables.

Provider Colors #

Each provider has a dedicated color used throughout the UI for avatars, message bubbles, borders, and accents:

Provider Color Variable Default Hex Avatar
ChatGPT --openai #10a37f
Claude --claude #cc785c
Gemini --gemini #4285f4
DeepSeek --deepseek #a855f7

Core CSS Variables #

Variable Purpose
--bg Main background color
--surface Card/panel background color
--border Border color
--text Primary text color
--muted Secondary/dimmed text color
--control-height Interactive element height (buttons, inputs)
--gutter Element spacing
--radius Border radius
--title-gradient Nav brand text gradient

Available Themes #

Theme Style
dark Default dark mode
light Light mode with subtle backgrounds
spring Green, pink, blue, purple accents
summer Cyan, gold, sky, purple accents
autumn Orange, pink, gold, purple — warm tones
winter Green, blue hues — cool and crisp
matrix Green monochrome terminal aesthetic
ice Cyan and blue frosty tones
sunset Pink, gold, blue, purple gradient feel
neon Bright neon accents on dark background
dracula Classic Dracula color scheme
solarized Ethan Schoonover's Solarized palette
midnight Dark with bright accent colors
aurora Cyan, green, purple, pink — northern lights
ember Warm orange and red tones
ocean Deep blue oceanic tones
forest Natural green forest palette
mono Grayscale monochrome

Data Persistence #

All data is stored in the local application data directory of the user running the host process:

%LOCALAPPDATA%\MindAttic\ThinkTank\
+-- Settings.json              # All app settings
+-- Personalities/             # Personality markdown files
|   +-- {templateId}.md
+-- Conversations/
    +-- {chatId}/
        +-- chat.json          # Append-only event log
        +-- {participantId}.md # Perspective files

When deployed to Azure App Service, %LOCALAPPDATA% maps under the App Service home directory (typically D:\local\LocalAppData\MindAttic\ThinkTank\). API keys should be promoted out of Settings.json and into Application Settings + Key Vault via the Vault overlay.

Settings.json Structure #

{
  "ProviderAuth": {
    "openai": "{ \"type\": \"bearer\", \"apiKey\": \"...\", \"model\": \"gpt-4o\", \"maxTokens\": 2048 }",
    "claude": "{ \"type\": \"anthropic\", \"apiKey\": \"...\", \"model\": \"claude-sonnet-4-6\", \"maxTokens\": 2048 }",
    "gemini": "{ \"type\": \"google\", \"apiKey\": \"...\", \"model\": \"gemini-2.5-flash\", \"maxTokens\": 2048 }",
    "deepseek": "{ \"type\": \"bearer\", \"apiKey\": \"...\", \"model\": \"deepseek-chat\", \"maxTokens\": 2048 }"
  },
  "Templates": [ ... ],
  "Conversations": [ ... ],
  "AppearanceTheme": "dark",
  "ControlHeight": 40,
  "Gutter": 10,
  "BorderRadius": 10
}

What Is Restored on Restart #

chat.json Event Types #

Type Description
chat-start Conversation initialization with chatId and topic
turn AI participant response with participantId, providerId, round, text
user User-injected message with round and text

JavaScript Interop #

The app uses JS interop for DOM operations that Blazor can't handle natively:

Function Location Purpose
setTheme(mode) theme.js Sets html[data-theme] attribute
setControlHeight(px) theme.js Updates --control-height CSS variable
setGutter(px) theme.js Updates --gutter CSS variable
setBorderRadius(px) theme.js Updates --radius CSS variable
isNearBottom(el, threshold) App.razor Checks if element is scrolled near bottom
scrollToBottom(el) App.razor Scrolls element to bottom
blurActive() App.razor Blurs the currently focused element

Supported Providers #

Every LLM call leaves Think Tank through MindAttic.Legion.LegionClient — Think Tank itself does not hold a single HTTP endpoint. Provider HTTP details (URLs, auth headers, request shape, retries, timeouts) all live in Legion's LlmProviderCatalog and per-provider clients.

Provider Auth Default Model
Claude (Anthropic) x-api-key header claude-sonnet-4-6
ChatGPT (OpenAI) Bearer token gpt-4.1-mini
Gemini (Google) API key (query parameter) gemini-2.5-flash
DeepSeek Bearer token (OpenAI-compatible) deepseek-chat
Mistral Bearer token mistral-large-latest
Grok (xAI) Bearer token (OpenAI-compatible) grok-3-mini-fast
Groq Bearer token (OpenAI-compatible) llama-3.3-70b-versatile
Together AI Bearer token meta-llama/Llama-3-70b-chat-hf
OpenRouter Bearer token meta-llama/llama-3.1-8b-instruct:free
Fireworks Bearer token accounts/fireworks/models/llama-v3p1-70b-instruct
Cohere Bearer token command-r-plus

By default, the UI surfaces the first-party set (claude, openai, gemini, deepseek); the remaining seven are opt-in by adding an entry in Settings > Providers.


Security #