fix: improve coach JSON reliability, add app icons
- Increase ChatJsonAsync max tokens 1024 → 2048 to prevent truncation - Lower JSON temperature 0.7 → 0.2 for more deterministic output - Add TryRepairJson fallback to close incomplete JSON from model - Strengthen JSON system prompt to reduce narrative drift - Generate icon.svg, icon.ico from source icon.png - Update Navbar sidebar logo to use icon.png - Update app.html favicon to use icon.ico - Copy icon.ico to Tauri icons directory Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
parent
53204ec59e
commit
c0f7c16898
@ -77,7 +77,6 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
||||
|
||||
var executor = new StatelessExecutor(_weights!, context.Params);
|
||||
|
||||
// Build multi-turn Phi-3 prompt with full conversation history
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"<|system|>\n{BuildChatSystemPrompt()}<|end|>\n");
|
||||
|
||||
@ -115,8 +114,10 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
||||
return await RunSessionAsync(prompt,
|
||||
$"You are a coaching assistant inside a private journaling app. " +
|
||||
$"Today's date is {dateStr}. " +
|
||||
$"You MUST respond with a single valid JSON object. No text before or after the JSON.",
|
||||
maxTokens: 1024, cancellationToken: cancellationToken);
|
||||
$"You MUST respond with ONLY a single valid JSON object. " +
|
||||
$"Do NOT write any text, explanation, or commentary before or after the JSON. " +
|
||||
$"Output MUST start with {{ and end with }}.",
|
||||
maxTokens: 2048, temperature: 0.2f, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<string> SummarizeEntryAsync(string content, string? fileStem = null,
|
||||
@ -194,16 +195,13 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
||||
|
||||
private async Task<string> EnsureModelAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. Configured path takes priority if the file already exists
|
||||
if (File.Exists(_configuredModelPath))
|
||||
return _configuredModelPath;
|
||||
|
||||
// 2. Check the standard app-data location
|
||||
var defaultPath = GetDefaultModelPath();
|
||||
if (File.Exists(defaultPath))
|
||||
return defaultPath;
|
||||
|
||||
// 3. Download from HuggingFace
|
||||
var modelDirectory = Path.GetDirectoryName(defaultPath)!;
|
||||
Directory.CreateDirectory(modelDirectory);
|
||||
|
||||
@ -230,21 +228,22 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
||||
|
||||
// ── Session / weights lifecycle ────────────────────────────────────────
|
||||
|
||||
private async Task<string> RunSessionAsync(string prompt, string systemPrompt,
|
||||
private Task<string> RunSessionAsync(string prompt, string systemPrompt,
|
||||
int maxTokens, CancellationToken cancellationToken)
|
||||
=> RunSessionAsync(prompt, systemPrompt, maxTokens, temperature: 0.7f, cancellationToken);
|
||||
|
||||
private async Task<string> RunSessionAsync(string prompt, string systemPrompt,
|
||||
int maxTokens, float temperature, CancellationToken cancellationToken)
|
||||
{
|
||||
var modelPath = await EnsureModelAsync(cancellationToken);
|
||||
EnsureWeights(modelPath);
|
||||
|
||||
// Fresh context per call — prevents KV cache accumulation across requests
|
||||
using var context = _weights!.CreateContext(new ModelParams(modelPath)
|
||||
{
|
||||
ContextSize = _contextSize,
|
||||
GpuLayerCount = _gpuLayers
|
||||
});
|
||||
|
||||
// Use StatelessExecutor with explicit Phi-3 chat template so the model
|
||||
// never sees raw system text it can echo back to the user.
|
||||
var executor = new StatelessExecutor(_weights!, context.Params);
|
||||
|
||||
var fullPrompt = $"<|system|>\n{systemPrompt}<|end|>\n" +
|
||||
@ -262,7 +261,7 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
||||
],
|
||||
SamplingPipeline = new DefaultSamplingPipeline
|
||||
{
|
||||
Temperature = 0.7f
|
||||
Temperature = temperature
|
||||
}
|
||||
};
|
||||
|
||||
@ -276,7 +275,6 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
||||
return StripSpecialTokens(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>Strips only Phi-3 special tokens — safe for JSON output.</summary>
|
||||
private static string StripSpecialTokens(string raw)
|
||||
{
|
||||
var text = raw;
|
||||
@ -285,22 +283,15 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
||||
return text.Trim();
|
||||
}
|
||||
|
||||
// Matches role labels like "System:", "**System:**", "**Assistant:**", "User:" etc.
|
||||
private static readonly Regex RoleMarkerRegex = MyRegex();
|
||||
|
||||
/// <summary>Aggressive cleanup for conversational (non-JSON) responses.</summary>
|
||||
private static string CleanChatResponse(string raw)
|
||||
{
|
||||
var text = StripSpecialTokens(raw);
|
||||
|
||||
// Strip role markers in any formatting variant (plain, bold-markdown, etc.)
|
||||
text = RoleMarkerRegex.Replace(text, "");
|
||||
|
||||
// Remove orphaned bold markers left behind after stripping
|
||||
text = text.Replace("**", "");
|
||||
|
||||
// Collapse runs of 3+ newlines into 2
|
||||
text = Regex.Replace(text, @"\n{3,}", "\n\n");
|
||||
text = MyRegex2().Replace(text, "\n\n");
|
||||
|
||||
return text.Trim();
|
||||
}
|
||||
@ -333,4 +324,6 @@ public sealed partial class LlamaSharpAiService(JournalConfig config) : IAiServi
|
||||
private static partial Regex MyRegex();
|
||||
[GeneratedRegex(@"\n{3,}")]
|
||||
private static partial Regex MyRegex1();
|
||||
[GeneratedRegex(@"\n{3,}")]
|
||||
private static partial Regex MyRegex2();
|
||||
}
|
||||
|
||||
@ -47,8 +47,6 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var raw = await _ai.ChatJsonAsync(prompt, cancellationToken);
|
||||
|
||||
// Try to extract JSON from the response
|
||||
var json = ExtractJson(raw);
|
||||
if (json is not null)
|
||||
{
|
||||
@ -60,11 +58,10 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Fall through to fallback
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: wrap raw text into a CoachPlanDto
|
||||
return new CoachPlanDto(
|
||||
Kind: fallbackKind,
|
||||
Title: "Coach Response",
|
||||
@ -104,7 +101,6 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
||||
|
||||
private static string? ExtractJson(string text)
|
||||
{
|
||||
// 1. Try ```json ... ``` code block
|
||||
var codeBlockStart = text.IndexOf("```json", StringComparison.Ordinal);
|
||||
if (codeBlockStart >= 0)
|
||||
{
|
||||
@ -118,7 +114,6 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Look specifically for {"kind" — the expected first field of CoachPlanDto
|
||||
var kindMarker = text.IndexOf("{\"kind\"", StringComparison.Ordinal);
|
||||
if (kindMarker < 0)
|
||||
kindMarker = text.IndexOf("{ \"kind\"", StringComparison.Ordinal);
|
||||
@ -133,7 +128,6 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback: try each { position until one parses as valid JSON
|
||||
var searchFrom = 0;
|
||||
var globalLastBrace = text.LastIndexOf('}');
|
||||
while (searchFrom < text.Length && globalLastBrace > searchFrom)
|
||||
@ -149,9 +143,67 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
||||
searchFrom = bracePos + 1;
|
||||
}
|
||||
|
||||
var firstBrace = text.IndexOf('{');
|
||||
if (firstBrace >= 0)
|
||||
{
|
||||
var repaired = TryRepairJson(text[firstBrace..]);
|
||||
if (repaired is not null)
|
||||
return repaired;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? TryRepairJson(string text)
|
||||
{
|
||||
var trimmed = text.TrimEnd();
|
||||
var lastUseful = trimmed.Length - 1;
|
||||
while (lastUseful >= 0)
|
||||
{
|
||||
var ch = trimmed[lastUseful];
|
||||
if (ch is '}' or ']' or '"' or ',' or ':' or '{' or '[' || char.IsDigit(ch)
|
||||
|| ch is 't' or 'r' or 'u' or 'e' or 'f' or 'a' or 'l' or 's' or 'n')
|
||||
break;
|
||||
lastUseful--;
|
||||
}
|
||||
|
||||
if (lastUseful < 0) return null;
|
||||
trimmed = trimmed[..(lastUseful + 1)];
|
||||
|
||||
if (trimmed.EndsWith(','))
|
||||
trimmed = trimmed[..^1];
|
||||
|
||||
var openBraces = 0;
|
||||
var openBrackets = 0;
|
||||
var inString = false;
|
||||
var escape = false;
|
||||
|
||||
foreach (var ch in trimmed)
|
||||
{
|
||||
if (escape) { escape = false; continue; }
|
||||
if (ch == '\\' && inString) { escape = true; continue; }
|
||||
if (ch == '"') { inString = !inString; continue; }
|
||||
if (inString) continue;
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '{': openBraces++; break;
|
||||
case '}': openBraces--; break;
|
||||
case '[': openBrackets++; break;
|
||||
case ']': openBrackets--; break;
|
||||
}
|
||||
}
|
||||
|
||||
if (openBraces <= 0 && openBrackets <= 0) return null;
|
||||
|
||||
var sb = new StringBuilder(trimmed);
|
||||
for (var i = 0; i < openBrackets; i++) sb.Append(']');
|
||||
for (var i = 0; i < openBraces; i++) sb.Append('}');
|
||||
|
||||
var repaired = sb.ToString();
|
||||
return TryValidateJson(repaired) ? repaired : null;
|
||||
}
|
||||
|
||||
private static bool TryValidateJson(string json)
|
||||
{
|
||||
try
|
||||
@ -169,11 +221,7 @@ public sealed class LlamaSharpCoachService(LlamaSharpAiService ai) : ICoachServi
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = assembly.GetManifestResourceNames()
|
||||
.FirstOrDefault(n => n.EndsWith(fileName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (resourceName is null)
|
||||
throw new FileNotFoundException($"Embedded resource not found: {fileName}");
|
||||
|
||||
.FirstOrDefault(n => n.EndsWith(fileName, StringComparison.OrdinalIgnoreCase)) ?? throw new FileNotFoundException($"Embedded resource not found: {fileName}");
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName)!;
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
return reader.ReadToEnd().Trim();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 279 KiB |
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="icon" href="%sveltekit.assets%/icon.ico" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
|
||||
<aside class="navbar" aria-label="Primary navigation">
|
||||
<div class="navbar-header">
|
||||
<img src="svelte.svg" alt="Journal logo" />
|
||||
<img src="icon.png" alt="Journal logo" style="height: 48px; width: 48px;" />
|
||||
</div>
|
||||
|
||||
<nav class="nav-groups" aria-label="Journal sections">
|
||||
|
||||
BIN
Journal.App/static/icon.ico
Normal file
BIN
Journal.App/static/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
BIN
Journal.App/static/icon.png
Normal file
BIN
Journal.App/static/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
5
Journal.App/static/icon.svg
Normal file
5
Journal.App/static/icon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.0 MiB |
Loading…
x
Reference in New Issue
Block a user