Improve settings hints and workspace log clarity

This commit is contained in:
2026-02-21 12:29:53 -03:00
parent a18545fb18
commit df401b9e55
4 changed files with 92 additions and 6 deletions

View File

@@ -9,4 +9,4 @@ This directory contains technical documentation for DMS.
- `api-contract.md` - API endpoint contract grouped by route module, including settings and processing-log trim defaults - `api-contract.md` - API endpoint contract grouped by route module, including settings and processing-log trim defaults
- `data-model-reference.md` - database entity definitions and lifecycle states - `data-model-reference.md` - database entity definitions and lifecycle states
- `operations-and-configuration.md` - runtime operations, ports, volumes, and persisted settings configuration - `operations-and-configuration.md` - runtime operations, ports, volumes, and persisted settings configuration
- `frontend-design-foundation.md` - frontend visual system, tokens, and UI implementation rules - `frontend-design-foundation.md` - frontend visual system, tokens, UI implementation rules, and settings helper-copy guidance

View File

@@ -50,3 +50,10 @@ When adding or redesigning a UI area:
3. Implement component styles in `frontend/src/styles.css` using existing layout and variant conventions. 3. Implement component styles in `frontend/src/styles.css` using existing layout and variant conventions.
4. Validate responsive behavior at `1240px`, `1040px`, `760px`, and `560px` breakpoints. 4. Validate responsive behavior at `1240px`, `1040px`, `760px`, and `560px` breakpoints.
5. Verify keyboard focus visibility and text contrast before merging. 5. Verify keyboard focus visibility and text contrast before merging.
## Settings UX Copy
- Keep helper copy in settings short and plain language, especially on advanced model and threshold controls.
- Prefer one concise hint per advanced control that explains practical impact rather than internals.
- In the Workspace settings block, keep processing-log controls visually separated from default path and tag behavior.
- Processing-log hints must explicitly state they affect logs and retention behavior, not document metadata values.

View File

@@ -1,5 +1,6 @@
/** /**
* Dedicated settings screen for providers, task model bindings, and catalog controls. * Dedicated settings screen for providers, task model bindings, and catalog controls.
* Uses concise helper hints for advanced runtime and provider settings.
*/ */
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import type { JSX } from 'react'; import type { JSX } from 'react';
@@ -67,7 +68,7 @@ function clampProcessingLogUnboundEntries(value: number): number {
} }
/** /**
* Renders compact human-oriented settings controls. * Renders compact human-oriented settings controls with plain-language hints.
*/ */
export default function SettingsScreen({ export default function SettingsScreen({
settings, settings,
@@ -367,6 +368,7 @@ export default function SettingsScreen({
onChange={(nextPath) => setUploadDefaults({ ...uploadDefaults, logical_path: nextPath })} onChange={(nextPath) => setUploadDefaults({ ...uploadDefaults, logical_path: nextPath })}
suggestions={knownPaths} suggestions={knownPaths}
/> />
<span className="settings-field-hint">Used when you upload without choosing a path.</span>
</label> </label>
<label className="settings-field settings-field-wide"> <label className="settings-field settings-field-wide">
Default Tags Default Tags
@@ -375,6 +377,7 @@ export default function SettingsScreen({
onChange={(nextTags) => setUploadDefaults({ ...uploadDefaults, tags: nextTags })} onChange={(nextTags) => setUploadDefaults({ ...uploadDefaults, tags: nextTags })}
suggestions={knownTags} suggestions={knownTags}
/> />
<span className="settings-field-hint">Added automatically when no tags are selected.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Cards Per Page Cards Per Page
@@ -385,7 +388,14 @@ export default function SettingsScreen({
value={cardsPerPageInput} value={cardsPerPageInput}
onChange={(event) => setCardsPerPageInput(event.target.value)} onChange={(event) => setCardsPerPageInput(event.target.value)}
/> />
<span className="settings-field-hint">Controls how many documents you see at once.</span>
</label> </label>
<div className="settings-subsection-divider">
<h4>Processing Log Controls</h4>
<p className="small">
These settings affect processing logs only. They do not change default path, tags, or document cards.
</p>
</div>
<label className="settings-field"> <label className="settings-field">
Keep document sessions Keep document sessions
<input <input
@@ -403,6 +413,7 @@ export default function SettingsScreen({
} }
}} }}
/> />
<span className="settings-field-hint">How many recent log sessions to keep for each document.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Keep unbound entries Keep unbound entries
@@ -421,8 +432,9 @@ export default function SettingsScreen({
} }
}} }}
/> />
<span className="settings-field-hint">How many standalone log entries to keep.</span>
</label> </label>
<label className="inline-checkbox settings-checkbox-field"> <label className="inline-checkbox settings-checkbox-field settings-checkbox-with-hint">
<input <input
type="checkbox" type="checkbox"
checked={displaySettings.log_typing_animation_enabled} checked={displaySettings.log_typing_animation_enabled}
@@ -430,7 +442,10 @@ export default function SettingsScreen({
setDisplaySettings({ ...displaySettings, log_typing_animation_enabled: event.target.checked }) setDisplaySettings({ ...displaySettings, log_typing_animation_enabled: event.target.checked })
} }
/> />
<span className="settings-checkbox-copy">
Processing log typing animation enabled Processing log typing animation enabled
<span className="settings-field-hint">Shows new log text as a type-in animation.</span>
</span>
</label> </label>
<p className="small settings-helper-text"> <p className="small settings-helper-text">
Processing-log retention values are used by backend trim routines when pruning historical entries. Processing-log retention values are used by backend trim routines when pruning historical entries.
@@ -565,6 +580,7 @@ export default function SettingsScreen({
) )
} }
/> />
<span className="settings-field-hint">Task settings use this ID to select the provider.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Label Label
@@ -596,6 +612,7 @@ export default function SettingsScreen({
); );
}} }}
/> />
<span className="settings-field-hint">Stop waiting after this many seconds if a call hangs.</span>
</label> </label>
<label className="settings-field settings-field-wide"> <label className="settings-field settings-field-wide">
Base URL Base URL
@@ -609,6 +626,7 @@ export default function SettingsScreen({
) )
} }
/> />
<span className="settings-field-hint">API endpoint root for this provider.</span>
</label> </label>
<label className="settings-field settings-field-wide"> <label className="settings-field settings-field-wide">
API Key API Key
@@ -624,6 +642,7 @@ export default function SettingsScreen({
) )
} }
/> />
<span className="settings-field-hint">Leave blank to keep the current stored key.</span>
</label> </label>
<label className="inline-checkbox settings-checkbox-field"> <label className="inline-checkbox settings-checkbox-field">
<input <input
@@ -678,10 +697,12 @@ export default function SettingsScreen({
<label className="settings-field"> <label className="settings-field">
Model Model
<input value={ocrTask.model} onChange={(event) => setOcrTask({ ...ocrTask, model: event.target.value })} /> <input value={ocrTask.model} onChange={(event) => setOcrTask({ ...ocrTask, model: event.target.value })} />
<span className="settings-field-hint">Model name sent to the selected provider.</span>
</label> </label>
<label className="settings-field settings-field-wide"> <label className="settings-field settings-field-wide">
OCR Prompt OCR Prompt
<textarea value={ocrTask.prompt} onChange={(event) => setOcrTask({ ...ocrTask, prompt: event.target.value })} /> <textarea value={ocrTask.prompt} onChange={(event) => setOcrTask({ ...ocrTask, prompt: event.target.value })} />
<span className="settings-field-hint">Instructions used when reading handwriting text.</span>
</label> </label>
</div> </div>
</div> </div>
@@ -708,6 +729,7 @@ export default function SettingsScreen({
<label className="settings-field"> <label className="settings-field">
Model Model
<input value={summaryTask.model} onChange={(event) => setSummaryTask({ ...summaryTask, model: event.target.value })} /> <input value={summaryTask.model} onChange={(event) => setSummaryTask({ ...summaryTask, model: event.target.value })} />
<span className="settings-field-hint">Model name sent to the selected provider.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Max Input Tokens Max Input Tokens
@@ -723,10 +745,12 @@ export default function SettingsScreen({
} }
}} }}
/> />
<span className="settings-field-hint">Long inputs are trimmed to this size before summarizing.</span>
</label> </label>
<label className="settings-field settings-field-wide"> <label className="settings-field settings-field-wide">
Summary Prompt Summary Prompt
<textarea value={summaryTask.prompt} onChange={(event) => setSummaryTask({ ...summaryTask, prompt: event.target.value })} /> <textarea value={summaryTask.prompt} onChange={(event) => setSummaryTask({ ...summaryTask, prompt: event.target.value })} />
<span className="settings-field-hint">Instructions that shape the generated summary.</span>
</label> </label>
</div> </div>
</div> </div>
@@ -753,42 +777,56 @@ export default function SettingsScreen({
<label className="settings-field"> <label className="settings-field">
Model Model
<input value={routingTask.model} onChange={(event) => setRoutingTask({ ...routingTask, model: event.target.value })} /> <input value={routingTask.model} onChange={(event) => setRoutingTask({ ...routingTask, model: event.target.value })} />
<span className="settings-field-hint">Model name sent to the selected provider.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Neighbor Count Neighbor Count
<input type="number" value={routingTask.neighbor_count} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_count: Number.parseInt(event.target.value, 10) || routingTask.neighbor_count })} /> <input type="number" value={routingTask.neighbor_count} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_count: Number.parseInt(event.target.value, 10) || routingTask.neighbor_count })} />
<span className="settings-field-hint">How many close matches to compare before routing.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Min Neighbor Similarity Min Neighbor Similarity
<input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_min_similarity} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_min_similarity: Number.parseFloat(event.target.value) || routingTask.neighbor_min_similarity })} /> <input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_min_similarity} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_min_similarity: Number.parseFloat(event.target.value) || routingTask.neighbor_min_similarity })} />
<span className="settings-field-hint">Ignore neighbors below this match score.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Auto Apply Confidence Auto Apply Confidence
<input type="number" step="0.01" min="0" max="1" value={routingTask.auto_apply_confidence_threshold} onChange={(event) => setRoutingTask({ ...routingTask, auto_apply_confidence_threshold: Number.parseFloat(event.target.value) || routingTask.auto_apply_confidence_threshold })} /> <input type="number" step="0.01" min="0" max="1" value={routingTask.auto_apply_confidence_threshold} onChange={(event) => setRoutingTask({ ...routingTask, auto_apply_confidence_threshold: Number.parseFloat(event.target.value) || routingTask.auto_apply_confidence_threshold })} />
<span className="settings-field-hint">Minimum model confidence for automatic changes.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Auto Apply Neighbor Similarity Auto Apply Neighbor Similarity
<input type="number" step="0.01" min="0" max="1" value={routingTask.auto_apply_neighbor_similarity_threshold} onChange={(event) => setRoutingTask({ ...routingTask, auto_apply_neighbor_similarity_threshold: Number.parseFloat(event.target.value) || routingTask.auto_apply_neighbor_similarity_threshold })} /> <input type="number" step="0.01" min="0" max="1" value={routingTask.auto_apply_neighbor_similarity_threshold} onChange={(event) => setRoutingTask({ ...routingTask, auto_apply_neighbor_similarity_threshold: Number.parseFloat(event.target.value) || routingTask.auto_apply_neighbor_similarity_threshold })} />
<span className="settings-field-hint">Minimum neighbor score for automatic changes.</span>
</label> </label>
<label className="inline-checkbox settings-checkbox-field"> <label className="inline-checkbox settings-checkbox-field settings-checkbox-with-hint">
<input type="checkbox" checked={routingTask.neighbor_path_override_enabled} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_enabled: event.target.checked })} /> <input type="checkbox" checked={routingTask.neighbor_path_override_enabled} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_enabled: event.target.checked })} />
<span className="settings-checkbox-copy">
Dominant neighbor path override enabled Dominant neighbor path override enabled
<span className="settings-field-hint">
If a strong top match disagrees with the model, use the top match path instead.
</span>
</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Override Min Similarity Override Min Similarity
<input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_path_override_min_similarity} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_min_similarity: Number.parseFloat(event.target.value) || routingTask.neighbor_path_override_min_similarity })} /> <input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_path_override_min_similarity} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_min_similarity: Number.parseFloat(event.target.value) || routingTask.neighbor_path_override_min_similarity })} />
<span className="settings-field-hint">Top neighbor must reach this score to override.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Override Min Gap Override Min Gap
<input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_path_override_min_gap} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_min_gap: Number.parseFloat(event.target.value) || routingTask.neighbor_path_override_min_gap })} /> <input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_path_override_min_gap} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_min_gap: Number.parseFloat(event.target.value) || routingTask.neighbor_path_override_min_gap })} />
<span className="settings-field-hint">Top match must beat the second match by at least this gap.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Override Max LLM Confidence Override Max LLM Confidence
<input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_path_override_max_confidence} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_max_confidence: Number.parseFloat(event.target.value) || routingTask.neighbor_path_override_max_confidence })} /> <input type="number" step="0.01" min="0" max="1" value={routingTask.neighbor_path_override_max_confidence} onChange={(event) => setRoutingTask({ ...routingTask, neighbor_path_override_max_confidence: Number.parseFloat(event.target.value) || routingTask.neighbor_path_override_max_confidence })} />
<span className="settings-field-hint">Override only runs when model confidence is at or below this level.</span>
</label> </label>
<label className="settings-field settings-field-wide"> <label className="settings-field settings-field-wide">
Routing Prompt Routing Prompt
<textarea value={routingTask.prompt} onChange={(event) => setRoutingTask({ ...routingTask, prompt: event.target.value })} /> <textarea value={routingTask.prompt} onChange={(event) => setRoutingTask({ ...routingTask, prompt: event.target.value })} />
<span className="settings-field-hint">Instructions used when deciding document path and tags.</span>
</label> </label>
</div> </div>
</div> </div>
@@ -805,26 +843,32 @@ export default function SettingsScreen({
<label className="settings-field settings-field-wide"> <label className="settings-field settings-field-wide">
Typesense Embedding Model Slug Typesense Embedding Model Slug
<input value={handwritingStyle.embed_model} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, embed_model: event.target.value })} /> <input value={handwritingStyle.embed_model} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, embed_model: event.target.value })} />
<span className="settings-field-hint">Embedding model used to compare handwriting style similarity.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Neighbor Limit Neighbor Limit
<input type="number" min={1} max={32} value={handwritingStyle.neighbor_limit} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, neighbor_limit: Number.parseInt(event.target.value, 10) || handwritingStyle.neighbor_limit })} /> <input type="number" min={1} max={32} value={handwritingStyle.neighbor_limit} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, neighbor_limit: Number.parseInt(event.target.value, 10) || handwritingStyle.neighbor_limit })} />
<span className="settings-field-hint">How many nearby samples to check during matching.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Match Min Similarity Match Min Similarity
<input type="number" step="0.01" min="0" max="1" value={handwritingStyle.match_min_similarity} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, match_min_similarity: Number.parseFloat(event.target.value) || handwritingStyle.match_min_similarity })} /> <input type="number" step="0.01" min="0" max="1" value={handwritingStyle.match_min_similarity} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, match_min_similarity: Number.parseFloat(event.target.value) || handwritingStyle.match_min_similarity })} />
<span className="settings-field-hint">Minimum similarity needed to treat two styles as a match.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Bootstrap Match Min Similarity Bootstrap Match Min Similarity
<input type="number" step="0.01" min="0" max="1" value={handwritingStyle.bootstrap_match_min_similarity} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, bootstrap_match_min_similarity: Number.parseFloat(event.target.value) || handwritingStyle.bootstrap_match_min_similarity })} /> <input type="number" step="0.01" min="0" max="1" value={handwritingStyle.bootstrap_match_min_similarity} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, bootstrap_match_min_similarity: Number.parseFloat(event.target.value) || handwritingStyle.bootstrap_match_min_similarity })} />
<span className="settings-field-hint">Stricter match score used only while bootstrapping new clusters.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Bootstrap Sample Size Bootstrap Sample Size
<input type="number" min={1} max={30} value={handwritingStyle.bootstrap_sample_size} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, bootstrap_sample_size: Number.parseInt(event.target.value, 10) || handwritingStyle.bootstrap_sample_size })} /> <input type="number" min={1} max={30} value={handwritingStyle.bootstrap_sample_size} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, bootstrap_sample_size: Number.parseInt(event.target.value, 10) || handwritingStyle.bootstrap_sample_size })} />
<span className="settings-field-hint">Number of samples used to start each new style cluster.</span>
</label> </label>
<label className="settings-field"> <label className="settings-field">
Max Image Side (px) Max Image Side (px)
<input type="number" min={256} max={4096} value={handwritingStyle.image_max_side} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, image_max_side: Number.parseInt(event.target.value, 10) || handwritingStyle.image_max_side })} /> <input type="number" min={256} max={4096} value={handwritingStyle.image_max_side} onChange={(event) => setHandwritingStyle({ ...handwritingStyle, image_max_side: Number.parseInt(event.target.value, 10) || handwritingStyle.image_max_side })} />
<span className="settings-field-hint">Resizes large images to this limit before analysis.</span>
</label> </label>
</div> </div>
</div> </div>

View File

@@ -976,6 +976,32 @@ button:disabled {
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.settings-field-hint {
margin: 0;
font-size: 0.72rem;
font-weight: 400;
color: #95a6c4;
line-height: 1.35;
}
.settings-subsection-divider {
grid-column: span 12;
display: grid;
gap: 0.2rem;
padding-top: 0.2rem;
border-top: 1px solid rgba(70, 89, 122, 0.55);
}
.settings-subsection-divider h4 {
margin: 0;
font-family: var(--font-display);
font-size: 0.82rem;
}
.settings-subsection-divider p {
margin: 0;
}
.inline-checkbox { .inline-checkbox {
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
@@ -1004,6 +1030,15 @@ button:disabled {
color: #dbe8ff; color: #dbe8ff;
} }
.settings-checkbox-with-hint {
align-items: start;
}
.settings-checkbox-copy {
display: grid;
gap: 0.15rem;
}
.settings-catalog-grid { .settings-catalog-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));