Skip to content

Commit de74d18

Browse files
author
Piotr Stachaczynski
committed
feat: polishing ui improvements and system prompts
1 parent 86422a5 commit de74d18

11 files changed

Lines changed: 894 additions & 64 deletions

File tree

backend/MaIN.Docs.Api/Models/CapacitySettings.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ namespace MaIN.Docs.Api.Models;
33
public record CapacitySettings(
44
Tier1Settings Tier1,
55
Tier2Settings Tier2,
6-
Tier3Settings Tier3)
6+
Tier3Settings Tier3,
7+
string ReasoningEffort = "")
78
{
8-
public CapacitySettings() : this(new Tier1Settings(), new Tier2Settings(), new Tier3Settings()) { }
9+
public CapacitySettings() : this(new Tier1Settings(), new Tier2Settings(), new Tier3Settings(), "") { }
910
}
1011

1112
public record Tier1Settings(long TokenLimit = 150_000, int CooldownMinutes = 180)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace MaIN.Docs.Api.Models;
2+
3+
public record ModelSettings(
4+
Tier1ModelSettings Tier1,
5+
Tier2ModelSettings Tier2)
6+
{
7+
public ModelSettings() : this(new Tier1ModelSettings(), new Tier2ModelSettings()) { }
8+
}
9+
10+
public record Tier1ModelSettings(
11+
string Chatty = "gemini-3.1-flash-lite",
12+
string Code = "gemini-3.5-flash",
13+
string Review = "gemini-3.5-flash",
14+
string Design = "gemini-3.1-pro-preview",
15+
string Forge = "gemini-3.1-pro-preview")
16+
{
17+
public Tier1ModelSettings() : this("gemini-3.1-flash-lite", "gemini-3.5-flash", "gemini-3.5-flash", "gemini-3.1-pro-preview", "gemini-3.1-pro-preview") { }
18+
}
19+
20+
public record Tier2ModelSettings(
21+
string Chatty = "gemini-3.1-flash-lite",
22+
string Code = "gemini-3.1-flash-lite",
23+
string Review = "gemini-3.1-flash-lite",
24+
string Design = "gemini-3.1-flash-lite",
25+
string Forge = "gemini-3.1-flash-lite")
26+
{
27+
public Tier2ModelSettings() : this("gemini-3.1-flash-lite", "gemini-3.1-flash-lite", "gemini-3.1-flash-lite", "gemini-3.1-flash-lite", "gemini-3.1-flash-lite") { }
28+
}

backend/MaIN.Docs.Api/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
});
4444

4545
builder.Services.Configure<CapacitySettings>(builder.Configuration.GetSection("Capacity"));
46+
builder.Services.Configure<ModelSettings>(builder.Configuration.GetSection("Models"));
4647
builder.Services.AddSingleton<CapacityStateStore>();
4748
builder.Services.AddSingleton<CapacityService>();
4849
builder.Services.AddHostedService<CapacityPersistenceService>();

backend/MaIN.Docs.Api/Services/DocsAgentOrchestrator.cs

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using MaIN.Domain.Models.Abstract;
1212
using MaIN.Docs.Api.Models;
1313
using Microsoft.Extensions.Options;
14-
using DomainModels = MaIN.Domain.Models.Models;
1514

1615
namespace MaIN.Docs.Api.Services;
1716

@@ -21,6 +20,7 @@ public class DocsAgentOrchestrator(
2120
GitHubService githubService,
2221
CapacityService capacityService,
2322
IOptions<CapacitySettings> capacityOptions,
23+
IOptions<ModelSettings> modelOptions,
2424
IConfiguration configuration,
2525
ILogger<DocsAgentOrchestrator> logger)
2626
{
@@ -44,6 +44,7 @@ private async Task InitializeForTierAsync(int tier)
4444
PrTools.Init(githubService);
4545

4646
var settings = capacityOptions.Value;
47+
var models = modelOptions.Value;
4748
var geminiKey1 = configuration["MaIN:GeminiKey"] ?? "";
4849

4950
string modelChatty, modelCode, modelReview, modelDesign, modelForge;
@@ -81,11 +82,11 @@ private async Task InitializeForTierAsync(int tier)
8182
cfg.BackendType = BackendType.Gemini;
8283
cfg.GeminiKey = settings.Tier2.GeminiKey;
8384
});
84-
modelChatty = DomainModels.Gemini.Gemini3_1FlashLite;
85-
modelCode = DomainModels.Gemini.Gemini3_1FlashLite;
86-
modelReview = DomainModels.Gemini.Gemini3_1FlashLite;
87-
modelDesign = DomainModels.Gemini.Gemini3_1FlashLite;
88-
modelForge = DomainModels.Gemini.Gemini3_1FlashLite;
85+
modelChatty = models.Tier2.Chatty;
86+
modelCode = models.Tier2.Code;
87+
modelReview = models.Tier2.Review;
88+
modelDesign = models.Tier2.Design;
89+
modelForge = models.Tier2.Forge;
8990
logger.LogInformation("[Capacity] Initializing agents for Tier 2 (low) — Flash Lite on secondary key");
9091
}
9192
else
@@ -95,11 +96,11 @@ private async Task InitializeForTierAsync(int tier)
9596
cfg.BackendType = BackendType.Gemini;
9697
cfg.GeminiKey = geminiKey1;
9798
});
98-
modelChatty = DomainModels.Gemini.Gemini3_1FlashLite;
99-
modelCode = DomainModels.Gemini.Gemini3_5Flash;
100-
modelReview = DomainModels.Gemini.Gemini3_5Flash;
101-
modelDesign = DomainModels.Gemini.Gemini3_1ProPreview;
102-
modelForge = DomainModels.Gemini.Gemini3_1ProPreview;
99+
modelChatty = models.Tier1.Chatty;
100+
modelCode = models.Tier1.Code;
101+
modelReview = models.Tier1.Review;
102+
modelDesign = models.Tier1.Design;
103+
modelForge = models.Tier1.Forge;
103104
logger.LogInformation("[Capacity] Initializing agents for Tier 1 (normal) — standard Gemini stack");
104105
}
105106

@@ -108,7 +109,13 @@ private async Task InitializeForTierAsync(int tier)
108109
// output. With no MaxTokens set, that budget can be exhausted entirely by reasoning
109110
// across multi-step tool-call turns, leaving both Content and the FullAnswer token
110111
// empty. Give every Gemini-backed agent more headroom.
111-
var geminiParams = tier < 3 ? new GeminiInferenceParams { MaxTokens = 16384 } : null;
112+
var geminiAdditionalParams = new Dictionary<string, object>();
113+
if (!string.IsNullOrWhiteSpace(settings.ReasoningEffort))
114+
geminiAdditionalParams["reasoning_effort"] = settings.ReasoningEffort;
115+
116+
var geminiParams = tier < 3
117+
? new GeminiInferenceParams { MaxTokens = 16384, AdditionalParams = geminiAdditionalParams }
118+
: null;
112119

113120
var chattyTools = BuildChattyTools(docsPath);
114121
var codeTools = BuildCodeTools(docsPath);
@@ -234,6 +241,8 @@ public async Task<AgentResult> ProcessAsync(
234241
docsRead.Add(path);
235242
toolResultChars += contentLength;
236243
});
244+
var readPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
245+
MdTools.SetReadDedup(readPaths);
237246

238247
if (agentId == "code")
239248
{
@@ -373,6 +382,7 @@ async Task<AgentResult> Core(IAgentContextExecutor agentCtx)
373382

374383
docsRead.Clear();
375384
toolResultChars = 0;
385+
readPaths.Clear();
376386
artifactUrl = null; artifactProposed = null;
377387
issueUrl = null; issueProposed = null; planProposed = null;
378388
reviewProposed = null; reviewPosted = null; codeChangeProposed = null;
@@ -397,6 +407,7 @@ async Task<AgentResult> Core(IAgentContextExecutor agentCtx)
397407
PrTools.SetPrCapture(null);
398408
PrTools.SetPrUrlCapture(null);
399409
MdTools.SetReadCapture(null);
410+
MdTools.SetReadDedup(null);
400411
sem.Release();
401412
}
402413
}
@@ -865,7 +876,7 @@ private static ToolsConfiguration BuildCodeTools(string docsPath) =>
865876
required = new[] { "archiveName", "description", "kind" }
866877
},
867878
ArtifactTools.Propose)
868-
.WithMaxIterations(10)
879+
.WithMaxIterations(18)
869880
.WithToolChoice("auto")
870881
.Build();
871882

@@ -1243,7 +1254,10 @@ FRAMEWORK FACTS (don't guess, verify via tools):
12431254
- Console bootstrap: MaINBootstrapper.Initialize(); ASP.NET: services.AddMaIN(configuration)
12441255
- MCP has 3 integration styles: direct prompt, agent pipeline step, RAG knowledge source
12451256
1246-
TOOL ECONOMY: You have 10 tool slots. list_docs (1) → read app-template doc + maybe 1 more (2) → show_file × N files (N) → propose_artifact_generation (1) = done. Never read the same file twice.
1257+
TOOL ECONOMY: You have 18 tool slots. list_docs (1) → read app-template doc + maybe 1 more (2) → show_file × N files (N) → propose_artifact_generation (1) = done.
1258+
NEVER call read_md_file twice for the same path — its content is already in an earlier tool
1259+
result in this conversation, re-requesting it just wastes your tool budget and gets you closer
1260+
to running out before propose_artifact_generation.
12471261
12481262
TOOLS — use them every time, no improvising:
12491263
1. list_docs — discover what files exist
@@ -1315,6 +1329,18 @@ before writing any code. Do not assume console.
13151329
solution (showing the new files per the ARTIFACTS rules) before calling
13161330
propose_artifact_generation / generate_artifact again.
13171331
1332+
DESKTOP VISUAL QUALITY — when kind is "desktop", app-template-desktop.md's "Modern
1333+
visual style — gradients & glass panels" and "Common AVLN2000 / XAML compile errors"
1334+
sections are part of the SAME read_md_file call as the rest of the template — apply
1335+
both. Pick an accent palette and layout shape that fit THIS app's topic (a weather
1336+
app, a recipe app, a budgeting app must not all look like the same blue chat window):
1337+
use a gradient header/hero, "glass" Border cards, and an accent color on primary
1338+
buttons, and shape the main layout (dashboard grid, list+detail, single tool view —
1339+
not always Chat+Settings tabs) around the app's actual feature. Before calling
1340+
show_file on any .axaml file, check every Padding/CornerRadius/BoxShadow/Spacing
1341+
usage against the AVLN2000 pitfalls section — wrap panels in a Border rather than
1342+
setting those directly on StackPanel/Grid/WrapPanel/DockPanel.
1343+
13181344
CONFIG PROMPTING — every generated app MUST let the user supply backend config (model
13191345
path/Ollama URL for local, API keys + model names for cloud) at runtime, never hardcoded:
13201346
- console: interactive Console.ReadLine() setup wizard (see app-template-console.md)

backend/MaIN.Docs.Api/Services/MdTools.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public static class MdTools
88
private static string _directory = "";
99
private static ILogger _logger = NullLogger.Instance;
1010
private static Action<string, int>? _readCapture;
11+
private static HashSet<string>? _readPaths;
1112

1213
public static void Initialize(string directory, ILogger logger)
1314
{
@@ -18,6 +19,10 @@ public static void Initialize(string directory, ILogger logger)
1819
// Invoked with (path, contentLength) so the orchestrator can fold tool-result size into the token estimate.
1920
public static void SetReadCapture(Action<string, int>? capture) => _readCapture = capture;
2021

22+
// Tracks paths already read during this turn so a repeat read_md_file call for the same
23+
// file doesn't re-spend tokens/iteration budget on content the model already has.
24+
public static void SetReadDedup(HashSet<string>? readPaths) => _readPaths = readPaths;
25+
2126
public record ListDocsArgs;
2227
public record SearchArgs(string Query);
2328
public record ReadArgs(string Path);
@@ -97,6 +102,17 @@ public static async Task<object> Read(ReadArgs args)
97102
return new { error = $"File not found: {args.Path}" };
98103
}
99104

105+
if (_readPaths is not null && !_readPaths.Add(args.Path))
106+
{
107+
_logger.LogInformation("[tool:read_md_file] Skipped duplicate read of {File} — already read this turn", Path.GetFileName(args.Path));
108+
return new
109+
{
110+
path = args.Path,
111+
fileName = Path.GetFileName(args.Path),
112+
note = "Already read earlier in this turn — its full content is in an earlier tool result above. Do not request it again."
113+
};
114+
}
115+
100116
var content = await File.ReadAllTextAsync(args.Path);
101117
_logger.LogInformation("[tool:read_md_file] Read {Bytes} chars from {File}", content.Length, Path.GetFileName(args.Path));
102118
_readCapture?.Invoke(args.Path, content.Length);

backend/MaIN.Docs.Api/appsettings.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,24 @@
1515
"Capacity": {
1616
"Tier1": { "TokenLimit": 150000, "CooldownMinutes": 180 },
1717
"Tier2": { "GeminiKey": "", "TokenLimit": 1000000, "CooldownMinutes": 120 },
18-
"Tier3": { "OllamaKey": "", "OllamaKey2": "", "OllamaModel": "gemma4:31b-cloud" }
18+
"Tier3": { "OllamaKey": "", "OllamaKey2": "", "OllamaModel": "gemma4:31b-cloud" },
19+
"ReasoningEffort": ""
20+
},
21+
"Models": {
22+
"Tier1": {
23+
"Chatty": "gemini-3.1-flash-lite",
24+
"Code": "gemini-3.5-flash",
25+
"Review": "gemini-3.5-flash",
26+
"Design": "gemini-3.1-pro-preview",
27+
"Forge": "gemini-3.1-pro-preview"
28+
},
29+
"Tier2": {
30+
"Chatty": "gemini-3.1-flash-lite",
31+
"Code": "gemini-3.1-flash-lite",
32+
"Review": "gemini-3.1-flash-lite",
33+
"Design": "gemini-3.1-flash-lite",
34+
"Forge": "gemini-3.1-flash-lite"
35+
}
1936
},
2037
"Logging": {
2138
"LogLevel": {

docs-content/app-template-desktop.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,107 @@ property) live in `Avalonia.Platform.Storage`. There is no standalone type
465465
called `FileTypeFilter` — using that name as a type causes
466466
`CS0246: cannot find type or namespace 'FileTypeFilter'`.
467467

468+
## Common AVLN2000 / XAML compile errors — avoid these
469+
470+
Avalonia's XAML compiler (AVLN2000 "Unable to resolve property X on type Y") is much
471+
stricter about which properties exist on which controls than WPF. These are the most
472+
common hallucinated combinations — check for them before calling `show_file`:
473+
474+
- **`Padding` does NOT exist on `Panel`-derived elements**`StackPanel`, `Grid`,
475+
`WrapPanel`, `DockPanel`, `Canvas` have no `Padding` property. Only `Border`,
476+
`Decorator`, and `ContentControl`-derived elements (`Button`, `TextBox`,
477+
`ContentControl`, `ScrollViewer`, `TabItem`, etc.) have `Padding`.
478+
- To pad the contents of a `StackPanel`/`Grid`, wrap it in `<Border Padding="20">...</Border>`,
479+
or set `Margin` on the panel itself.
480+
- **`Spacing` only exists on `StackPanel` and `WrapPanel`** — not `Grid` or `DockPanel`.
481+
Use `Margin` on individual children for spacing in those.
482+
- **`CornerRadius` exists on `Border` and a few controls** (`Button`, `TextBox`, etc. —
483+
anything templated on a rounded `ContentPresenter`/`Border`).
484+
- **`BoxShadow` exists ONLY on `Border`.** It is NOT a property of `Button`, `TextBox`,
485+
`Image`, or any other control — `<Button BoxShadow="...">` is an AVLN2000 error. If a
486+
button (or any other control) needs a shadow, wrap it in a `<Border BoxShadow="...">`,
487+
or skip the shadow on that element entirely.
488+
- Rule of thumb: if you're adding `Padding`, `BoxShadow`, or a `Background` for visual
489+
effect to a `StackPanel`/`Grid`/`WrapPanel`/`DockPanel`/`Button`/other non-`Border`
490+
control, wrap that element in a `Border` and set the property on the `Border` instead.
491+
`CornerRadius` is the only one of these that's safe directly on `Button`/`TextBox`.
492+
493+
## Modern visual style — gradients & glass panels
494+
495+
The default `FluentTheme` on its own looks flat and generic, and reusing the exact
496+
layout/palette from this template for every app is why generated apps look "boring and
497+
all the same". Layer these simple, verified techniques on top of `FluentTheme` (don't
498+
replace it) and pick a palette that fits the app's actual topic — a weather app, a
499+
recipe app, and a budgeting app should NOT end up with the same blue/gray look.
500+
501+
### 1. App-wide accent palette in App.axaml
502+
503+
```xml
504+
<Application xmlns="https://github.com/avaloniaui"
505+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
506+
x:Class="ChatDesktop.App">
507+
<Application.Styles>
508+
<FluentTheme />
509+
</Application.Styles>
510+
<Application.Resources>
511+
<SolidColorBrush x:Key="AccentBrush">#6366F1</SolidColorBrush>
512+
<LinearGradientBrush x:Key="HeroGradient" StartPoint="0%,0%" EndPoint="100%,100%">
513+
<GradientStop Color="#6366F1" Offset="0" />
514+
<GradientStop Color="#EC4899" Offset="1" />
515+
</LinearGradientBrush>
516+
</Application.Resources>
517+
</Application>
518+
```
519+
520+
Pick 2 colors that fit the app's domain instead of always reusing indigo/pink —
521+
e.g. sky/cyan for weather, emerald/teal for finance or health, amber/orange for
522+
productivity, violet/fuchsia for creative tools.
523+
524+
### 2. Gradient header / hero panel
525+
526+
```xml
527+
<Border Background="{StaticResource HeroGradient}" CornerRadius="0,0,16,16" Padding="24,18">
528+
<TextBlock Text="Aura Weather" FontSize="22" FontWeight="Bold" Foreground="White" />
529+
</Border>
530+
```
531+
532+
### 3. "Glass" cards
533+
534+
```xml
535+
<Border Background="#1AFFFFFF" CornerRadius="16" Padding="18" BoxShadow="0 4 24 0 #40000000">
536+
<!-- card content -->
537+
</Border>
538+
```
539+
540+
`#1AFFFFFF` is white at ~10% opacity (ARGB hex, alpha first) — a translucent overlay
541+
that reads as "glass" on both dark and light window backgrounds. Increase the alpha
542+
(e.g. `#33FFFFFF`) for more contrast against a busy background.
543+
544+
### 4. Window background
545+
546+
Give the window a deliberate base color instead of the default flat gray, so the
547+
gradient/glass elements have something to sit on top of:
548+
549+
```xml
550+
<Window ... Background="#0F172A">
551+
```
552+
553+
### 5. Accent buttons
554+
555+
```xml
556+
<Button Content="Refresh" Background="{StaticResource AccentBrush}" Foreground="White"
557+
CornerRadius="8" Padding="14,8" />
558+
```
559+
560+
### Vary the layout too, not just the colors
561+
562+
This template's TabControl (Chat + Settings) is the right shape when the app's core
563+
feature genuinely is an AI chat. If the app's main feature is something else (a
564+
dashboard, a list + detail view, a single-page tool), build the layout that fits that
565+
feature — a `Grid` dashboard of `Border` "glass" cards, a master/detail `Grid` with two
566+
columns, etc. — and put the MaIN.NET config (Settings) behind a small gear `Button`
567+
that opens a second `Window`, rather than forcing every app into the same two-tab shape.
568+
468569
## Customizing
469570

470571
- **Non-chat UI**: replace the Chat `TabItem` content with whatever controls suit

0 commit comments

Comments
 (0)