Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions src-tauri/src/models/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub struct AppSettings {
pub default_thinking: bool,
pub first_run_completed: bool,
pub check_for_updates: bool,
pub high_contrast: bool,
pub language: Option<String>,
pub model_directory: Option<String>,
pub backend_working_directory: Option<String>,
Expand Down Expand Up @@ -93,6 +94,7 @@ impl Default for AppSettings {
default_thinking: true,
first_run_completed: false,
check_for_updates: true,
high_contrast: false,
language: None,
model_directory: None,
backend_working_directory: None,
Expand Down Expand Up @@ -168,6 +170,7 @@ pub enum SettingKey {
DefaultThinking,
FirstRunCompleted,
CheckForUpdates,
HighContrast,
Language,
ModelDirectory,
BackendWorkingDirectory,
Expand All @@ -188,6 +191,7 @@ impl SettingKey {
"defaultThinking" => Ok(Self::DefaultThinking),
"firstRunCompleted" => Ok(Self::FirstRunCompleted),
"checkForUpdates" => Ok(Self::CheckForUpdates),
"highContrast" => Ok(Self::HighContrast),
"language" => Ok(Self::Language),
"modelDirectory" => Ok(Self::ModelDirectory),
"backendWorkingDirectory" => Ok(Self::BackendWorkingDirectory),
Expand All @@ -211,6 +215,7 @@ impl SettingKey {
Self::DefaultThinking => "defaultThinking",
Self::FirstRunCompleted => "firstRunCompleted",
Self::CheckForUpdates => "checkForUpdates",
Self::HighContrast => "highContrast",
Self::Language => "language",
Self::ModelDirectory => "modelDirectory",
Self::BackendWorkingDirectory => "backendWorkingDirectory",
Expand Down Expand Up @@ -289,6 +294,11 @@ impl AppSettings {
AppError::validation_failed(format!("invalid checkForUpdates value: {error}"))
})?;
}
SettingKey::HighContrast => {
self.high_contrast = serde_json::from_value(value).map_err(|error| {
AppError::validation_failed(format!("invalid highContrast value: {error}"))
})?;
}
SettingKey::Language => {
self.language = serde_json::from_value(value).map_err(|error| {
AppError::validation_failed(format!("invalid language value: {error}"))
Expand Down Expand Up @@ -353,6 +363,7 @@ impl AppSettings {
"checkForUpdates",
serde_json::to_string(&self.check_for_updates),
),
("highContrast", serde_json::to_string(&self.high_contrast)),
("language", serde_json::to_string(&self.language)),
(
"modelDirectory",
Expand Down Expand Up @@ -404,6 +415,7 @@ mod tests {
"defaultThinking",
"firstRunCompleted",
"checkForUpdates",
"highContrast",
"language",
"modelDirectory",
"backendWorkingDirectory",
Expand Down
8 changes: 8 additions & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import * as api from "@/app/lib/api";
import { useGenerationStore } from "@/app/lib/store";
import { useAppMenuRuntime } from "@/app/runtime/menu-runtime";

function useHighContrast() {
const highContrast = useGenerationStore((s) => s.highContrast);
useEffect(() => {
document.documentElement.toggleAttribute("data-high-contrast", highContrast);
}, [highContrast]);
}

function BootShell() {
return (
<div className="flex h-screen w-full items-center justify-center bg-[#121212] text-[12px] tracking-wide text-[rgba(255,255,255,0.55)]">
Expand All @@ -17,6 +24,7 @@ function BootShell() {
}

function App() {
useHighContrast();
const { t } = useTranslation();
const { addToast } = useToast();
const hydrateFromPersistence = useGenerationStore((state) => state.hydrateFromPersistence);
Expand Down
11 changes: 8 additions & 3 deletions src/app/components/generation/GenerationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@ type ToggleField =
| "useCotLanguage"
| "constrainedDecoding";

function FieldError({ message }: { message?: string }) {
function FieldError({ message, id }: { message?: string; id?: string }) {
if (!message) return null;
return <p className="text-[11px] text-red-300">{message}</p>;
return (
<p id={id} className="text-[11px] text-red-300">
{message}
</p>
);
}

function FieldLabel({ children }: { children: React.ReactNode }) {
Expand Down Expand Up @@ -396,8 +400,9 @@ export function GenerationPanel() {
value={form.prompt}
onChange={handleTextFieldChange("prompt")}
disabled={isBusy}
aria-describedby={validationErrors.prompt ? "gen-error-prompt" : undefined}
/>
<FieldError message={validationErrors.prompt} />
<FieldError id="gen-error-prompt" message={validationErrors.prompt} />
</label>

{/* Model info - compact row */}
Expand Down
31 changes: 23 additions & 8 deletions src/app/components/generation/GenerationPanel/FormBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ export function FormBody({
value={form.prompt}
onChange={handleTextFieldChange("prompt")}
disabled={isBusy}
aria-describedby={validationErrors.prompt ? "error-prompt" : undefined}
/>
<FieldError message={validationErrors.prompt} />
<FieldError id="error-prompt" message={validationErrors.prompt} />
</label>

{/* Model info - compact row */}
Expand Down Expand Up @@ -223,8 +224,9 @@ export function FormBody({
value={form.lyrics}
onChange={handleTextFieldChange("lyrics")}
disabled={isBusy || form.instrumental}
aria-describedby={validationErrors.lyrics ? "error-lyrics" : undefined}
/>
<FieldError message={validationErrors.lyrics} />
<FieldError id="error-lyrics" message={validationErrors.lyrics} />
</div>

{/* Basic parameters - grouped */}
Expand All @@ -244,8 +246,9 @@ export function FormBody({
value={form.durationSeconds}
onChange={handleTextFieldChange("durationSeconds")}
disabled={isBusy}
aria-describedby={validationErrors.durationSeconds ? "error-duration" : undefined}
/>
<FieldError message={validationErrors.durationSeconds} />
<FieldError id="error-duration" message={validationErrors.durationSeconds} />
</label>
<label className="space-y-1">
<FieldLabel>{t("generation.bpm")}</FieldLabel>
Expand All @@ -271,9 +274,10 @@ export function FormBody({
value={form.bpm}
onChange={handleTextFieldChange("bpm")}
disabled={isBusy || form.bpmMode === "auto"}
aria-describedby={validationErrors.bpm ? "error-bpm" : undefined}
/>
</div>
<FieldError message={validationErrors.bpm} />
<FieldError id="error-bpm" message={validationErrors.bpm} />
</label>
<label className="space-y-1">
<FieldLabel>{t("generation.keyScale")}</FieldLabel>
Expand Down Expand Up @@ -366,7 +370,7 @@ export function FormBody({
contentClassName="border-t border-[var(--color-border-light)]"
>
<div className="space-y-4 p-4">
<p className="text-[12px] text-[var(--color-text-dim)]">
<p id="desc-tweak-sound" className="text-[12px] text-[var(--color-text-dim)]">
{t("generation.tweakSoundDesc")}
</p>
<div className="grid gap-3 md:grid-cols-2">
Expand All @@ -378,8 +382,13 @@ export function FormBody({
value={form.negativePrompt}
onChange={handleTextFieldChange("negativePrompt")}
disabled={isBusy}
aria-describedby={
validationErrors.negativePrompt
? "error-negative-prompt desc-tweak-sound"
: "desc-tweak-sound"
}
/>
<FieldError message={validationErrors.negativePrompt} />
<FieldError id="error-negative-prompt" message={validationErrors.negativePrompt} />
</label>

<label className="space-y-1">
Expand All @@ -391,8 +400,11 @@ export function FormBody({
value={form.inferenceSteps}
onChange={handleTextFieldChange("inferenceSteps")}
disabled={isBusy}
aria-describedby={
validationErrors.inferenceSteps ? "error-inference-steps" : undefined
}
/>
<FieldError message={validationErrors.inferenceSteps} />
<FieldError id="error-inference-steps" message={validationErrors.inferenceSteps} />
</label>
<label className="space-y-1">
<FieldLabel>{t("generation.guidanceScale")}</FieldLabel>
Expand All @@ -404,8 +416,11 @@ export function FormBody({
value={form.guidanceScale}
onChange={handleTextFieldChange("guidanceScale")}
disabled={isBusy}
aria-describedby={
validationErrors.guidanceScale ? "error-guidance-scale" : undefined
}
/>
<FieldError message={validationErrors.guidanceScale} />
<FieldError id="error-guidance-scale" message={validationErrors.guidanceScale} />
</label>
</div>

Expand Down
8 changes: 6 additions & 2 deletions src/app/components/generation/GenerationPanel/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ export type ToggleField =
| "useCotLanguage"
| "constrainedDecoding";

export function FieldError({ message }: { message?: string }) {
export function FieldError({ message, id }: { message?: string; id?: string }) {
if (!message) return null;
return <p className="text-[11px] text-red-300">{message}</p>;
return (
<p id={id} className="text-[11px] text-red-300">
{message}
</p>
);
}

export function FieldLabel({ children }: { children: React.ReactNode }) {
Expand Down
1 change: 1 addition & 0 deletions src/app/components/history/HistorySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,7 @@ function FailedRunsDrawer() {
<button
type="button"
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
className="flex w-full items-center justify-between text-[11px] font-semibold tracking-wide text-[var(--color-text-dim)]"
>
<span className="flex items-center gap-1.5">
Expand Down
10 changes: 3 additions & 7 deletions src/app/components/settings/SettingsOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,7 @@ export function SettingsOverlay() {

const handleSave = async () => {
if (!(await saveChanges())) {
setSaveNotice(
t("settings.backendPortInvalid", {
defaultValue: "Backend port must be between 1024 and 65535.",
}),
);
setSaveNotice(t("settings.backendPortInvalid"));
return;
}
await hydrateFromPersistence();
Expand All @@ -87,8 +83,8 @@ export function SettingsOverlay() {
{ id: "defaults", label: t("settings.defaults") },
{ id: "general", label: t("settings.general") },
{ id: "backend", label: t("settings.backend") },
{ id: "network", label: t("settings.networkActivity", { defaultValue: "Network" }) },
{ id: "logs", label: t("settings.appLogs", { defaultValue: "Logs" }) },
{ id: "network", label: t("settings.networkActivity") },
{ id: "logs", label: t("settings.appLogs") },
{ id: "danger", label: t("settings.danger") },
] as const;

Expand Down
45 changes: 12 additions & 33 deletions src/app/components/settings/SetupScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ function PackDownloadCard({
{state === "ready" ? (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/12 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-emerald-200">
<CheckCircle2 size={10} />
{t("setup.downloadComplete", { defaultValue: "Downloaded" })}
{t("setup.downloadedBadge")}
</span>
) : state === "failed" ? (
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/12 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-red-200">
Expand Down Expand Up @@ -199,7 +199,7 @@ function PackDownloadCard({
{state === "downloading" || busy ? (
<>
<Loader2 size={12} className="animate-spin" />
{t("setup.downloadingButton", { defaultValue: "Downloading…" })}
{t("setup.downloadingButton")}
</>
) : state === "ready" ? (
<>
Expand Down Expand Up @@ -404,10 +404,7 @@ export function SetupScreen({ onClose }: SetupScreenProps) {
<SetupActionCard
icon={Sparkles}
title={t("setup.downloadModel")}
description={t("setup.downloadModelDesc", {
defaultValue:
"Download the ACE-Step engine and a model weight pack to start generating.",
})}
description={t("setup.downloadModelDesc")}
/>
<SetupActionCard
icon={FolderOutput}
Expand Down Expand Up @@ -470,9 +467,7 @@ export function SetupScreen({ onClose }: SetupScreenProps) {
{/* Engine provisioning card */}
<div className="space-y-3">
<p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--color-text-dim)]">
{t("settings.backendEngine", {
defaultValue: "Backend engine",
})}
{t("settings.backendEngine")}
</p>
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-sidebar)] p-4">
<div className="flex flex-wrap items-start justify-between gap-3">
Expand All @@ -482,9 +477,7 @@ export function SetupScreen({ onClose }: SetupScreenProps) {
{backendProvisionStatus.state === "ready" ? (
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/12 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-emerald-200">
<CheckCircle2 size={10} />
{t("setup.downloadComplete", {
defaultValue: "Ready",
})}
{t("setup.engineReady")}
</span>
) : backendProvisionStatus.state === "failed" ? (
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/12 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-red-200">
Expand All @@ -494,9 +487,7 @@ export function SetupScreen({ onClose }: SetupScreenProps) {
) : null}
</div>
<p className="text-[11px] leading-5 text-[var(--color-text-dim)]">
{t("settings.backendEngineDescription", {
defaultValue: "The ACE-Step Python engine runs locally to generate music.",
})}
{t("settings.backendEngineDescription")}
</p>
{backendProvisionStatus.state === "ready" &&
backendProvisionStatus.installedTag ? (
Expand All @@ -519,9 +510,7 @@ export function SetupScreen({ onClose }: SetupScreenProps) {
backendProvisionStatus.state === "extracting" ? (
<>
<Loader2 size={12} className="animate-spin" />
{t("settings.provisioningBackend", {
defaultValue: "Downloading…",
})}
{t("settings.provisioningBackend")}
</>
) : backendProvisionStatus.state === "ready" ? (
<>
Expand Down Expand Up @@ -557,7 +546,7 @@ export function SetupScreen({ onClose }: SetupScreenProps) {

<div className="space-y-3">
<p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--color-text-dim)]">
{t("setup.modelPackHeading", { defaultValue: "Weight packs" })}
{t("setup.modelPackHeading")}
</p>
{(Object.keys(MODEL_PACKS) as ModelPackId[]).map((packId) => {
const pack = MODEL_PACKS[packId];
Expand All @@ -584,9 +573,7 @@ export function SetupScreen({ onClose }: SetupScreenProps) {

<div className="space-y-3">
<p className="text-[11px] font-semibold uppercase tracking-wide text-[var(--color-text-dim)]">
{t("setup.modelProfileHeading", {
defaultValue: "Runtime profiles",
})}
{t("setup.modelProfileHeading")}
</p>
<div className="grid gap-3 md:grid-cols-3">
{(["lite", "turbo", "pro"] as const).map((variant) => {
Expand Down Expand Up @@ -622,9 +609,7 @@ export function SetupScreen({ onClose }: SetupScreenProps) {
className="inline-flex items-center gap-1 text-[11px] text-[var(--color-text-dim)] underline decoration-[var(--color-text-dimmer)]/40 underline-offset-2 transition-colors hover:text-white hover:decoration-white/60 disabled:opacity-50"
>
{skipLoading ? <Loader2 size={10} className="animate-spin" /> : null}
{t("setup.skipDemo", {
defaultValue: "Skip and try a demo prompt",
})}
{t("setup.skipDemo")}
</button>
</div>
</div>
Expand Down Expand Up @@ -684,11 +669,7 @@ export function SetupScreen({ onClose }: SetupScreenProps) {
<div className="rounded-xl border border-[var(--color-border)] bg-[var(--color-sidebar)] p-4">
<div className="flex items-center gap-2 text-[var(--color-accent)]">
<Keyboard size={16} />
<p className="text-[13px] font-semibold text-white">
{t("setup.shortcutsHint", {
defaultValue: "Keyboard shortcuts",
})}
</p>
<p className="text-[13px] font-semibold text-white">{t("setup.shortcutsHint")}</p>
</div>
<div className="mt-3 grid gap-2 sm:grid-cols-2">
{(
Expand All @@ -707,9 +688,7 @@ export function SetupScreen({ onClose }: SetupScreenProps) {
className="flex items-center justify-between rounded-lg border border-[var(--color-border-light)] bg-[var(--color-surface)] px-3 py-2"
>
<span className="text-[12px] text-[var(--color-text-dim)]">
{t(`setup.shortcut_${labelKey}`, {
defaultValue: labelKey,
})}
{t(`setup.shortcut_${labelKey}`)}
</span>
<kbd className="rounded-md border border-[var(--color-border-light)] bg-[var(--color-surface-muted)] px-2 py-0.5 font-mono text-[11px] text-[var(--color-text)]">
{getShortcutDisplay(shortcut, platform)}
Expand Down
Loading
Loading