Release v0.3.6
Some checks are pending
CI / cargo check (linux) (push) Waiting to run
CI / cargo check (macos) (push) Waiting to run
CI / cargo check (windows) (push) Waiting to run
CI / tsc + hotkey smoke (push) Waiting to run

This commit is contained in:
David Perov 2026-07-02 18:14:59 +03:00
parent b83ea7254f
commit 2f0bc345c5
24 changed files with 572 additions and 261 deletions

View file

@ -40,6 +40,13 @@ It is designed for practical daily work: IDEs, chats, notes, CRM fields, email,
## Latest Changes
### v0.3.6
- Fixed permissions onboarding recovery for existing installs so updates do not reopen the full permissions screen when required macOS access is already granted.
- Switched the app icon set from Lucide to Phosphor through a local icon facade for softer, more consistent product UI icons.
- Added a macOS Vite/esbuild runner workaround for development and release builds from external drives.
- Kept local STT failures separate from API-key authentication errors when a local model is selected.
### v0.3.5
- Fixed local STT mode so managed localhost runtimes are called without a stale API bearer token and no longer show an API-key error for local runtime failures.

View file

@ -1,10 +1,10 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "talk-flow",
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-autostart": "^2.5.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
@ -15,7 +15,6 @@
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-store": "^2.4.2",
"@tauri-apps/plugin-updater": "^2.10.1",
"lucide-react": "^0.577.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
},
@ -135,6 +134,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
@ -361,8 +362,6 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],

View file

@ -0,0 +1,61 @@
# Release Review v0.3.6
## Release
- Version: 0.3.6
- Release branch: release/v0.3.6
- Target tag: v0.3.6
- Reviewer: Codex
- Date: 2026-07-02
## Scope
- Key changes included in this release:
- Recover completed permissions onboarding for existing installs when required macOS permissions are already granted.
- Replace Lucide icons with Phosphor icons through a local icon facade.
- Add a Vite runner that copies the macOS esbuild binary to the system temp directory before dev/build, avoiding `write EPIPE` failures when the repo lives on an external drive.
- Preserve the local-model API-key fix and widget notice alignment fix from the hotfix work.
- User-facing changes:
- Updates should no longer reopen the full permissions screen for users who already granted microphone and Accessibility access.
- Local STT mode should no longer surface API-key wording for local runtime failures.
- Settings and widget UI use the softer Phosphor icon set.
- `bun run tauri dev` works from `/Volumes/KINGSTON` without manual `ESBUILD_BINARY_PATH`.
- Risky areas:
- Permissions onboarding recovery relies on existing history plus current OS permission checks.
- Icon migration touches many UI imports, though through one facade.
- Local macOS updater signing requires `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`; the local environment did not provide it.
## Checks run
- `bun run check:release`: passed.
- `bun run build:release:macos`: app bundle and updater tarball built, then failed at updater signing because the local environment did not provide the private-key password.
- Native/GitHub Windows build: pending GitHub Actions tag build.
- Native/GitHub Linux build: pending GitHub Actions tag build.
- Additional manual checks:
- `bun run dev -- --host 127.0.0.1`: passed after the Vite/esbuild runner fix.
- `bunx tsc --noEmit`: passed during icon rollback.
- Verified `lucide-react`, `@tabler/icons-react`, and `@hugeicons/*` are absent from app dependencies and source.
## Manual review
- Hotkey flow: smoke test passed through `src/windows/widget/services/hotkeyFsm.test.js`.
- Onboarding permissions: reviewed startup gating in `SettingsApp`; system-audio permission is no longer required for the initial settings app gate, and existing installs with history can recover the completed flag.
- Widget position and notice behavior: reviewed notice icon facade usage and centered icon layout.
- Transcription quality and short-utterance handling: unchanged in this release.
- README refreshed: yes, `README.md` includes `v0.3.6` notes.
## Findings
- Blockers:
- None for GitHub Actions release, assuming repository signing secrets include the updater private key password.
- Non-blocking issues:
- Local `bun run build:release:macos` cannot complete updater signing without `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`.
- Production frontend bundle remains above the default Vite 500 kB warning threshold.
- Follow-ups after release:
- Consider documenting the local updater signing password requirement for macOS release builds.
- Consider code-splitting settings tabs or model assets to reduce the main frontend bundle size.
## Decision
- Ready for `main` merge: yes.
- Ready for tag publish: yes, with GitHub Actions signing secrets expected to complete updater signing.

View file

@ -1,7 +1,7 @@
{
"name": "talkis",
"private": true,
"version": "0.3.5",
"version": "0.3.6",
"license": "AGPL-3.0-or-later",
"type": "module",
"packageManager": "bun@1.2.13",
@ -9,8 +9,8 @@
"prepare:ffmpeg-sidecar": "bun scripts/prepare-ffmpeg-sidecar.mjs",
"prepare:stt-sidecar": "bun scripts/prepare-stt-sidecar.mjs",
"prepare:sidecars": "bun run prepare:ffmpeg-sidecar && bun run prepare:stt-sidecar",
"dev": "vite",
"build": "tsc && vite build",
"dev": "bun scripts/run-vite.mjs",
"build": "tsc && bun scripts/run-vite.mjs build",
"check": "bun run prepare:sidecars && bunx tsc --noEmit && cargo check --manifest-path src-tauri/Cargo.toml",
"test": "bun test",
"smoke:hotkey": "bun test ./src/windows/widget/services/hotkeyFsm.test.js",
@ -21,12 +21,13 @@
"build:release:macos": "bun scripts/build-release.mjs macos",
"build:release:windows": "bun scripts/build-release.mjs windows",
"build:release:linux": "bun scripts/build-release.mjs linux",
"preview": "vite preview",
"preview": "bun scripts/run-vite.mjs preview",
"tauri": "tauri",
"logs": "tail -f ~/.talkis/talkis.log",
"logs:clear": "rm -f ~/.talkis/talkis.log"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-autostart": "^2.5.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
@ -37,7 +38,6 @@
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-store": "^2.4.2",
"@tauri-apps/plugin-updater": "^2.10.1",
"lucide-react": "^0.577.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},

56
scripts/run-vite.mjs Normal file
View file

@ -0,0 +1,56 @@
import { spawn } from "node:child_process";
import { existsSync, mkdirSync, copyFileSync, chmodSync } from "node:fs";
import { dirname, join } from "node:path";
import { tmpdir } from "node:os";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const rootDir = dirname(dirname(new URL(import.meta.url).pathname));
function resolvePackageFile(packageName, relativePath) {
return join(dirname(require.resolve(`${packageName}/package.json`)), relativePath);
}
function prepareEsbuildBinary() {
if (process.platform !== "darwin") return undefined;
const source = resolvePackageFile("esbuild", "bin/esbuild");
if (!existsSync(source)) return undefined;
const targetDir = join(tmpdir(), "talkis-esbuild");
const target = join(targetDir, `esbuild-${process.platform}-${process.arch}`);
mkdirSync(targetDir, { recursive: true });
copyFileSync(source, target);
chmodSync(target, 0o755);
return target;
}
function resolveViteBin() {
const binName = process.platform === "win32" ? "vite.cmd" : "vite";
return join(rootDir, "node_modules", ".bin", binName);
}
const env = { ...process.env };
const esbuildBinary = prepareEsbuildBinary();
if (esbuildBinary) {
env.ESBUILD_BINARY_PATH = esbuildBinary;
}
const child = spawn(resolveViteBin(), process.argv.slice(2), {
cwd: rootDir,
env,
stdio: "inherit",
shell: process.platform === "win32",
});
child.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 1);
});

2
src-tauri/Cargo.lock generated
View file

@ -4969,7 +4969,7 @@ dependencies = [
[[package]]
name = "talkis"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"base64 0.22.1",
"block2 0.6.2",

View file

@ -1,6 +1,6 @@
[package]
name = "talkis"
version = "0.3.5"
version = "0.3.6"
description = "Talkis - Voice to Text Desktop Widget"
authors = ["trixter"]
license = "AGPL-3.0-or-later"

View file

@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "Talkis",
"mainBinaryName": "Talkis",
"version": "0.3.5",
"version": "0.3.6",
"identifier": "com.trixter.talkis",
"build": {
"beforeDevCommand": "bun run prepare:sidecars && bun run dev",

View file

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { Check, ChevronDown } from "lucide-react";
import { IconCheck, IconChevronDown } from "../lib/icons";
export interface DropdownOption {
value: string;
@ -59,9 +59,9 @@ export function Dropdown({
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{current?.label ?? value}
</span>
<ChevronDown
<IconChevronDown
size={14}
strokeWidth={2}
stroke={2}
style={{ flexShrink: 0, transform: open ? "rotate(180deg)" : "none", transition: "transform 0.15s" }}
/>
</button>
@ -113,7 +113,7 @@ export function Dropdown({
onMouseLeave={(e) => (e.currentTarget.style.background = active ? "var(--dropdown-active)" : "transparent")}
>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{option.label}</span>
{active && <Check size={14} strokeWidth={2.5} style={{ flexShrink: 0, color: "var(--text-hi)" }} />}
{active && <IconCheck size={14} stroke={2.5} style={{ flexShrink: 0, color: "var(--text-hi)" }} />}
</button>
);
})}

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { AlertCircle, Check, Download, HardDrive, Loader2, MemoryStick, Trash2, X } from "lucide-react";
import { IconAlertCircle, IconCheck, IconDownload, IconDeviceDesktopCog, IconLoader2, IconCpu, IconTrash, IconX } from "../lib/icons";
import { AppSettings } from "../lib/store";
import { useI18n } from "../lib/i18n";
@ -225,7 +225,7 @@ export function LocalLlmModels({
border: "1px solid var(--border-subtle)",
}}
>
<AlertCircle size={15} strokeWidth={2} style={{ flexShrink: 0, marginTop: 1, color: "var(--accent)" }} />
<IconAlertCircle size={15} stroke={2} style={{ flexShrink: 0, marginTop: 1, color: "var(--accent)" }} />
<span>{t("localLlm.required")}</span>
</div>
)}
@ -344,7 +344,7 @@ export function LocalLlmModels({
aria-label={t("localLlm.diskSize", { size: model.size_label })}
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
<HardDrive size={14} strokeWidth={1.9} color="var(--text-hi)" />
<IconDeviceDesktopCog size={14} stroke={1.9} color="var(--text-hi)" />
<span style={{ fontSize: 12, fontWeight: 650, color: "var(--text-hi)", lineHeight: 1 }}>
{model.size_label}
</span>
@ -354,7 +354,7 @@ export function LocalLlmModels({
aria-label={t("localLlm.ramRequired", { ram: model.min_ram_gb })}
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
<MemoryStick size={14} strokeWidth={1.9} color="var(--text-hi)" />
<IconCpu size={14} stroke={1.9} color="var(--text-hi)" />
<span style={{ fontSize: 12, fontWeight: 650, color: "var(--text-hi)", lineHeight: 1 }}>
{t("localLlm.ramShort", { ram: model.min_ram_gb })}
</span>
@ -442,7 +442,7 @@ export function LocalLlmModels({
color: "var(--text-hi)",
}}
>
<X size={14} strokeWidth={2.2} />
<IconX size={14} stroke={2.2} />
{t("localLlm.cancel")}
</button>
) : (
@ -454,7 +454,7 @@ export function LocalLlmModels({
color: "var(--accent-contrast)",
}}
>
<Download size={14} strokeWidth={2.2} />
<IconDownload size={14} stroke={2.2} />
{t("localLlm.download")}
</button>
)
@ -472,13 +472,13 @@ export function LocalLlmModels({
}}
>
{isBusy ? (
<Loader2
<IconLoader2
size={14}
strokeWidth={2.2}
stroke={2.2}
style={{ animation: "spin 1s linear infinite" }}
/>
) : (
<Check size={14} strokeWidth={2.5} />
<IconCheck size={14} stroke={2.5} />
)}
{t("localLlm.select")}
</button>
@ -492,7 +492,7 @@ export function LocalLlmModels({
color: "var(--text-hi)",
}}
>
<Trash2 size={14} strokeWidth={2.2} />
<IconTrash size={14} stroke={2.2} />
{t("localLlm.delete")}
</button>
</>

View file

@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { openPath } from "@tauri-apps/plugin-opener";
import { Mic, Keyboard, Check, AlertCircle, Volume2 } from "lucide-react";
import { IconMicrophone, IconKeyboard, IconCheck, IconAlertCircle, IconVolume } from "../lib/icons";
import {
PermissionStatus,
checkAccessibilityPermission,
@ -66,7 +66,7 @@ function PermissionRow({
flexShrink: 0,
}}
>
{isGranted ? <Check size={16} strokeWidth={2.5} /> : icon}
{isGranted ? <IconCheck size={16} stroke={2.5} /> : icon}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
@ -452,7 +452,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) {
border: "1px solid var(--danger-border)",
}}
>
<AlertCircle
<IconAlertCircle
size={16}
style={{
color: "var(--danger)",
@ -484,7 +484,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) {
)}
<PermissionRow
icon={<Mic size={16} strokeWidth={1.8} />}
icon={<IconMicrophone size={16} stroke={1.8} />}
title={t("permission.mic.title")}
description={t("permission.mic.desc")}
status={micStatus}
@ -493,7 +493,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) {
{requiresSystemAudio && (
<PermissionRow
icon={<Volume2 size={16} strokeWidth={1.8} />}
icon={<IconVolume size={16} stroke={1.8} />}
title={t("permission.systemAudio.title")}
description={t("permission.systemAudio.desc")}
status={systemAudioStatus}
@ -503,7 +503,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) {
)}
<PermissionRow
icon={<Keyboard size={16} strokeWidth={1.8} />}
icon={<IconKeyboard size={16} stroke={1.8} />}
title={pastePermissionTitle}
description={pastePermissionDescription}
status={
@ -540,7 +540,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) {
border: "1px solid var(--border-subtle)",
}}
>
<AlertCircle
<IconAlertCircle
size={14}
style={{
color: "var(--text-low)",

View file

@ -1,5 +1,5 @@
import { useEffect, useRef, useState, type ReactNode } from "react";
import { MoreHorizontal } from "lucide-react";
import { IconDots } from "../lib/icons";
export interface RowActionItem {
key: string;
@ -51,7 +51,7 @@ export function RowActionsMenu({
}}
style={{ width: 32, minWidth: 32, height: 32, minHeight: 32, padding: 0, borderRadius: 8, flexShrink: 0, justifyContent: "center" }}
>
<MoreHorizontal size={15} strokeWidth={2} />
<IconDots size={15} stroke={2} />
</button>
{open && (

View file

@ -1,5 +1,5 @@
import { useEffect, useState, type ReactNode } from "react";
import { X } from "lucide-react";
import { IconX } from "../lib/icons";
import { useI18n } from "../lib/i18n";
@ -98,7 +98,7 @@ export function SlideOverModal({
aria-label={t("summary.close")}
style={{ width: 32, minWidth: 32, minHeight: 32, padding: 0, justifyContent: "center", flexShrink: 0 }}
>
<X size={14} strokeWidth={2} />
<IconX size={14} stroke={2} />
</button>
</div>

View file

@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { listen } from "@tauri-apps/api/event";
import { Check, Clipboard, Pencil, Trash2 } from "lucide-react";
import { IconCheck, IconClipboard, IconPencil, IconTrash } from "../lib/icons";
import { SETTINGS_UPDATED_EVENT } from "../lib/hotkeyEvents";
import {
@ -295,9 +295,9 @@ export function SummaryModal({
<RowActionsMenu
label={t("summary.actions")}
items={[
{ key: "copy", label: copiedId === item.id ? t("summary.copied") : t("summary.copy"), icon: copiedId === item.id ? <Check size={14} strokeWidth={2.5} /> : <Clipboard size={14} strokeWidth={2} />, onSelect: () => void copy(item) },
{ key: "edit", label: t("summary.edit"), icon: <Pencil size={14} strokeWidth={2} />, onSelect: () => startEdit(item) },
{ key: "delete", label: t("summary.delete"), icon: <Trash2 size={14} strokeWidth={2} />, danger: true, onSelect: () => void remove(item) },
{ key: "copy", label: copiedId === item.id ? t("summary.copied") : t("summary.copy"), icon: copiedId === item.id ? <IconCheck size={14} stroke={2.5} /> : <IconClipboard size={14} stroke={2} />, onSelect: () => void copy(item) },
{ key: "edit", label: t("summary.edit"), icon: <IconPencil size={14} stroke={2} />, onSelect: () => startEdit(item) },
{ key: "delete", label: t("summary.delete"), icon: <IconTrash size={14} stroke={2} />, danger: true, onSelect: () => void remove(item) },
] as RowActionItem[]}
/>
</div>

View file

@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
import { openUrl } from "@tauri-apps/plugin-opener";
import { listen } from "@tauri-apps/api/event";
import { onOpenUrl } from "@tauri-apps/plugin-deep-link";
import { LogOut, User, Crown } from "lucide-react";
import { IconLogout, IconUser, IconCrown } from "../lib/icons";
import {
beginCloudAuthFlow,
@ -221,7 +221,7 @@ export function UserPanel() {
cancelLocalAuthPolling();
setProfile(null);
await cloudLogout();
logInfo("USER_PANEL", "User logged out");
logInfo("USER_PANEL", "IconUser logged out");
};
if (loading) {
@ -247,7 +247,7 @@ export function UserPanel() {
<div style={styles.container}>
<ProfileRow profile={profile} onLogout={handleLogout} />
<button onClick={handleActivate} style={styles.compactCta}>
<Crown size={13} strokeWidth={2} color="var(--accent-contrast)" />
<IconCrown size={13} stroke={2} color="var(--accent-contrast)" />
<span style={styles.compactCtaLabel}>{t("userPanel.upgradeToPro")}</span>
</button>
</div>
@ -274,7 +274,7 @@ function ProfileRow({ profile, onLogout }: { profile: CloudProfile; onLogout: ()
style={{ width: "100%", height: "100%", borderRadius: "50%", objectFit: "cover" }}
/>
) : (
<User size={16} strokeWidth={1.5} color="var(--text-low)" />
<IconUser size={16} stroke={1.5} color="var(--text-low)" />
)}
</div>
<div style={styles.profileInfo}>
@ -284,7 +284,7 @@ function ProfileRow({ profile, onLogout }: { profile: CloudProfile; onLogout: ()
<div style={styles.profileEmail}>{profile.user.email}</div>
</div>
<button onClick={onLogout} style={styles.logoutButton} title={t("userPanel.logout")}>
<LogOut size={14} strokeWidth={1.8} />
<IconLogout size={14} stroke={1.8} />
</button>
</div>
);
@ -295,7 +295,7 @@ function SubscriptionCTA({ onActivate }: { onActivate: () => void }) {
return (
<div style={styles.ctaBox}>
<div style={styles.ctaHeader}>
<Crown size={14} strokeWidth={2} color="var(--text-hi)" />
<IconCrown size={14} stroke={2} color="var(--text-hi)" />
<span style={{ fontWeight: 700, fontSize: 12, letterSpacing: "-0.02em", color: "var(--text-hi)" }}>
{t("userPanel.cta.title")}
</span>

180
src/lib/icons.ts Normal file
View file

@ -0,0 +1,180 @@
import type {
ComponentPropsWithoutRef,
ForwardRefExoticComponent,
RefAttributes,
} from "react";
import { SlidersHorizontalIcon as RawIconAdjustmentsHorizontal } from "@phosphor-icons/react/SlidersHorizontal";
import { WarningCircleIcon as RawIconAlertCircle } from "@phosphor-icons/react/WarningCircle";
import { WarningIcon as RawIconAlertTriangle } from "@phosphor-icons/react/Warning";
import { LightningIcon as RawIconBolt } from "@phosphor-icons/react/Lightning";
import { BriefcaseIcon as RawIconBriefcase } from "@phosphor-icons/react/Briefcase";
import { BroadcastIcon as RawIconBroadcast } from "@phosphor-icons/react/Broadcast";
import { CheckIcon as RawIconCheck } from "@phosphor-icons/react/Check";
import { CaretDownIcon as RawIconChevronDown } from "@phosphor-icons/react/CaretDown";
import { ClipboardIcon as RawIconClipboard } from "@phosphor-icons/react/Clipboard";
import { CloudIcon as RawIconCloud } from "@phosphor-icons/react/Cloud";
import { CodeIcon as RawIconCode } from "@phosphor-icons/react/Code";
import { CopyIcon as RawIconCopy } from "@phosphor-icons/react/Copy";
import { CpuIcon as RawIconCpu } from "@phosphor-icons/react/Cpu";
import { CrownIcon as RawIconCrown } from "@phosphor-icons/react/Crown";
import { DesktopIcon as RawIconDeviceDesktop } from "@phosphor-icons/react/Desktop";
import { GearIcon as RawIconDeviceDesktopCog } from "@phosphor-icons/react/Gear";
import { DotsThreeIcon as RawIconDots } from "@phosphor-icons/react/DotsThree";
import { DownloadSimpleIcon as RawIconDownload } from "@phosphor-icons/react/DownloadSimple";
import { ArrowSquareOutIcon as RawIconExternalLink } from "@phosphor-icons/react/ArrowSquareOut";
import { FileAudioIcon as RawIconFileMusic } from "@phosphor-icons/react/FileAudio";
import { GaugeIcon as RawIconGauge } from "@phosphor-icons/react/Gauge";
import { HeadphonesIcon as RawIconHeadphones } from "@phosphor-icons/react/Headphones";
import { QuestionIcon as RawIconHelpCircle } from "@phosphor-icons/react/Question";
import { HouseIcon as RawIconHome } from "@phosphor-icons/react/House";
import { InfoIcon as RawIconInfoCircle } from "@phosphor-icons/react/Info";
import { KeyboardIcon as RawIconKeyboard } from "@phosphor-icons/react/Keyboard";
import { TranslateIcon as RawIconLanguage } from "@phosphor-icons/react/Translate";
import { ListChecksIcon as RawIconListCheck } from "@phosphor-icons/react/ListChecks";
import { SpinnerGapIcon as RawIconLoader2 } from "@phosphor-icons/react/SpinnerGap";
import { SignOutIcon as RawIconLogout } from "@phosphor-icons/react/SignOut";
import { EnvelopeIcon as RawIconMail } from "@phosphor-icons/react/Envelope";
import { ChatCircleIcon as RawIconMessage } from "@phosphor-icons/react/ChatCircle";
import { MicrophoneIcon as RawIconMicrophone } from "@phosphor-icons/react/Microphone";
import { MoonIcon as RawIconMoon } from "@phosphor-icons/react/Moon";
import { PencilSimpleIcon as RawIconPencil } from "@phosphor-icons/react/PencilSimple";
import { PhoneCallIcon as RawIconPhoneCall } from "@phosphor-icons/react/PhoneCall";
import { PlayIcon as RawIconPlayerPlay } from "@phosphor-icons/react/Play";
import { PlusIcon as RawIconPlus } from "@phosphor-icons/react/Plus";
import { ArrowClockwiseIcon as RawIconRefresh } from "@phosphor-icons/react/ArrowClockwise";
import { ArrowsClockwiseIcon as RawIconRotate2 } from "@phosphor-icons/react/ArrowsClockwise";
import { MagnifyingGlassIcon as RawIconSearch } from "@phosphor-icons/react/MagnifyingGlass";
import { HardDrivesIcon as RawIconServer } from "@phosphor-icons/react/HardDrives";
import { SparkleIcon as RawIconSparkles } from "@phosphor-icons/react/Sparkle";
import { SpeakerHighIcon as RawIconSpeakerphone } from "@phosphor-icons/react/SpeakerHigh";
import { SquareIcon as RawIconSquare } from "@phosphor-icons/react/Square";
import { SunIcon as RawIconSun } from "@phosphor-icons/react/Sun";
import { TargetIcon as RawIconTargetArrow } from "@phosphor-icons/react/Target";
import { TrashIcon as RawIconTrash } from "@phosphor-icons/react/Trash";
import { TextTIcon as RawIconTypography } from "@phosphor-icons/react/TextT";
import { UserIcon as RawIconUser } from "@phosphor-icons/react/User";
import { SpeakerHighIcon as RawIconVolume } from "@phosphor-icons/react/SpeakerHigh";
import { XIcon as RawIconX } from "@phosphor-icons/react/X";
export interface IconProps
extends Omit<ComponentPropsWithoutRef<"svg">, "stroke">,
RefAttributes<SVGSVGElement> {
alt?: string;
color?: string;
mirrored?: boolean;
size?: number | string;
stroke?: number | string;
title?: string;
weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone";
}
export type Icon = ForwardRefExoticComponent<IconProps>;
const IconAdjustmentsHorizontal = RawIconAdjustmentsHorizontal as Icon;
const IconAlertCircle = RawIconAlertCircle as Icon;
const IconAlertTriangle = RawIconAlertTriangle as Icon;
const IconBolt = RawIconBolt as Icon;
const IconBriefcase = RawIconBriefcase as Icon;
const IconBroadcast = RawIconBroadcast as Icon;
const IconCheck = RawIconCheck as Icon;
const IconChevronDown = RawIconChevronDown as Icon;
const IconClipboard = RawIconClipboard as Icon;
const IconCloud = RawIconCloud as Icon;
const IconCode = RawIconCode as Icon;
const IconCopy = RawIconCopy as Icon;
const IconCpu = RawIconCpu as Icon;
const IconCrown = RawIconCrown as Icon;
const IconDeviceDesktop = RawIconDeviceDesktop as Icon;
const IconDeviceDesktopCog = RawIconDeviceDesktopCog as Icon;
const IconDots = RawIconDots as Icon;
const IconDownload = RawIconDownload as Icon;
const IconExternalLink = RawIconExternalLink as Icon;
const IconFileMusic = RawIconFileMusic as Icon;
const IconGauge = RawIconGauge as Icon;
const IconHeadphones = RawIconHeadphones as Icon;
const IconHelpCircle = RawIconHelpCircle as Icon;
const IconHome = RawIconHome as Icon;
const IconInfoCircle = RawIconInfoCircle as Icon;
const IconKeyboard = RawIconKeyboard as Icon;
const IconLanguage = RawIconLanguage as Icon;
const IconListCheck = RawIconListCheck as Icon;
const IconLoader2 = RawIconLoader2 as Icon;
const IconLogout = RawIconLogout as Icon;
const IconMail = RawIconMail as Icon;
const IconMessage = RawIconMessage as Icon;
const IconMicrophone = RawIconMicrophone as Icon;
const IconMoon = RawIconMoon as Icon;
const IconPencil = RawIconPencil as Icon;
const IconPhoneCall = RawIconPhoneCall as Icon;
const IconPlayerPlay = RawIconPlayerPlay as Icon;
const IconPlus = RawIconPlus as Icon;
const IconRefresh = RawIconRefresh as Icon;
const IconRotate2 = RawIconRotate2 as Icon;
const IconSearch = RawIconSearch as Icon;
const IconServer = RawIconServer as Icon;
const IconSparkles = RawIconSparkles as Icon;
const IconSpeakerphone = RawIconSpeakerphone as Icon;
const IconSquare = RawIconSquare as Icon;
const IconSun = RawIconSun as Icon;
const IconTargetArrow = RawIconTargetArrow as Icon;
const IconTrash = RawIconTrash as Icon;
const IconTypography = RawIconTypography as Icon;
const IconUser = RawIconUser as Icon;
const IconVolume = RawIconVolume as Icon;
const IconX = RawIconX as Icon;
export {
IconAdjustmentsHorizontal,
IconAlertCircle,
IconAlertTriangle,
IconBolt,
IconBriefcase,
IconBroadcast,
IconCheck,
IconChevronDown,
IconClipboard,
IconCloud,
IconCode,
IconCopy,
IconCpu,
IconCrown,
IconDeviceDesktop,
IconDeviceDesktopCog,
IconDots,
IconDownload,
IconExternalLink,
IconFileMusic,
IconGauge,
IconHeadphones,
IconHelpCircle,
IconHome,
IconInfoCircle,
IconKeyboard,
IconLanguage,
IconListCheck,
IconLoader2,
IconLogout,
IconMail,
IconMessage,
IconMicrophone,
IconMoon,
IconPencil,
IconPhoneCall,
IconPlayerPlay,
IconPlus,
IconRefresh,
IconRotate2,
IconSearch,
IconServer,
IconSparkles,
IconSpeakerphone,
IconSquare,
IconSun,
IconTargetArrow,
IconTrash,
IconTypography,
IconUser,
IconVolume,
IconX,
};

View file

@ -4,17 +4,17 @@ import { getVersion } from "@tauri-apps/api/app";
import { listen } from "@tauri-apps/api/event";
import { openUrl } from "@tauri-apps/plugin-opener";
import {
Download,
ExternalLink,
FileAudio,
Home,
Cpu,
Languages,
Loader2,
Sparkles,
Sliders,
LucideIcon,
} from "lucide-react";
IconDownload,
IconExternalLink,
IconFileMusic,
IconHome,
IconCpu,
IconLanguage,
IconLoader2,
IconSparkles,
IconAdjustmentsHorizontal,
Icon,
} from "../../lib/icons";
import { TitleBar } from "../../components/TitleBar";
import { MainTab } from "./tabs/MainTab";
import { FileTranscriptionTab } from "./tabs/FileTranscriptionTab";
@ -72,31 +72,31 @@ function resolveInitialTab(): Tab {
return "main";
}
const TABS: { id: Tab; labelKey: MsgKey; icon: LucideIcon; note: string }[] = [
{ id: "main", labelKey: "settingsApp.tab.main", icon: Home, note: "История записей" },
const TABS: { id: Tab; labelKey: MsgKey; icon: Icon; note: string }[] = [
{ id: "main", labelKey: "settingsApp.tab.main", icon: IconHome, note: "История записей" },
{
id: "file",
labelKey: "settingsApp.tab.file",
icon: FileAudio,
icon: IconFileMusic,
note: "Транскрибация",
},
{
id: "interpreter",
labelKey: "settingsApp.tab.interpreter",
icon: Languages,
icon: IconLanguage,
note: "Realtime Interpreter beta",
},
{
id: "model",
labelKey: "settingsApp.tab.model",
icon: Cpu,
icon: IconCpu,
note: "Ключи и подключение модели",
},
{ id: "style", labelKey: "settingsApp.tab.style", icon: Sparkles, note: "Стиль обработки и Промпты для саммари" },
{ id: "style", labelKey: "settingsApp.tab.style", icon: IconSparkles, note: "Стиль обработки и Промпты для саммари" },
{
id: "settings",
labelKey: "settingsApp.tab.settings",
icon: Sliders,
icon: IconAdjustmentsHorizontal,
note: "Язык, микрофон и горячая клавиша",
},
];
@ -119,7 +119,7 @@ function TabButton({
className={`nav-item ${isActive ? "active" : ""}`}
style={{ width: "100%", textAlign: "left", font: "inherit" }}
>
<Icon size={18} strokeWidth={isActive ? 2.2 : 1.6} />
<Icon size={18} stroke={isActive ? 2.2 : 1.6} />
<span>{t(tab.labelKey)}</span>
</button>
);
@ -267,16 +267,16 @@ function AppUpdateFooter(): ReactElement | null {
}}
>
{installing ? (
<Loader2
<IconLoader2
className="loading-soft-icon"
size={13}
strokeWidth={2}
stroke={2}
style={{
flexShrink: 0,
}}
/>
) : (
<Download size={13} strokeWidth={2} style={{ flexShrink: 0 }} />
<IconDownload size={13} stroke={2} style={{ flexShrink: 0 }} />
)}
<span>
{installing
@ -329,9 +329,9 @@ function AppUpdateFooter(): ReactElement | null {
<span style={{ display: "inline-flex", alignItems: "center" }}>
v{version}
</span>
<ExternalLink
<IconExternalLink
size={10}
strokeWidth={2}
stroke={2}
style={{ display: "block", marginTop: -1 }}
/>
</div>
@ -377,13 +377,21 @@ export function SettingsApp() {
useEffect(() => {
Promise.all([getPermissionsPassed(), checkAllPermissions(), getHistory()])
.then(([passed, permissions, history]) => {
const hasRequiredPermissions =
.then(async ([passed, permissions, history]) => {
const hasRequiredStartupPermissions =
permissions.microphone !== "denied" &&
permissions.accessibility === "granted" &&
(!isMacPlatform() || permissions.systemAudio === "granted");
(!isMacPlatform() || permissions.accessibility === "granted");
const shouldRecoverExistingInstall =
!passed && history.length > 0 && hasRequiredStartupPermissions;
if (shouldRecoverExistingInstall) {
await setPermissionsPassed(true);
}
setInitialHistory(history);
setShowPermissions(!(passed && hasRequiredPermissions));
setShowPermissions(
!((passed || shouldRecoverExistingInstall) && hasRequiredStartupPermissions),
);
setLoadError(null);
})
.catch((error) => {

View file

@ -5,14 +5,14 @@ import { emit, listen } from "@tauri-apps/api/event";
import { getCurrentWebview } from "@tauri-apps/api/webview";
import { open } from "@tauri-apps/plugin-dialog";
import {
AlertCircle,
Check,
Clipboard,
FileAudio,
ListChecks,
Loader2,
X,
} from "lucide-react";
IconAlertCircle,
IconCheck,
IconClipboard,
IconFileMusic,
IconListCheck,
IconLoader2,
IconX,
} from "../../../lib/icons";
import {
addHistoryEntry,
@ -585,7 +585,7 @@ export function FileTranscriptionTab({
if (speakerMode && !settings.useOwnKey && !cloudSpeakerReady) {
setSpeakerDiarization(false);
await saveSettings({ fileSpeakerDiarization: false });
throw new Error("Cloud speaker diarization unavailable");
throw new Error("IconCloud speaker diarization unavailable");
}
if (speakerMode && settings.useOwnKey && !isSpeakerSetupReady(settings)) {
@ -945,7 +945,7 @@ export function FileTranscriptionTab({
setSpeakerSetupIntent(null);
setError(
toFileTranscriptionErrorMessage(
new Error("Cloud speaker diarization unavailable"),
new Error("IconCloud speaker diarization unavailable"),
),
);
setSpeakerDiarization(false);
@ -1092,13 +1092,13 @@ export function FileTranscriptionTab({
}}
>
{isProcessing ? (
<Loader2
<IconLoader2
className="loading-soft-icon"
size={24}
strokeWidth={1.8}
stroke={1.8}
/>
) : (
<FileAudio size={25} strokeWidth={1.8} />
<IconFileMusic size={25} stroke={1.8} />
)}
</div>
@ -1166,9 +1166,9 @@ export function FileTranscriptionTab({
textAlign: "left",
}}
>
<AlertCircle
<IconAlertCircle
size={16}
strokeWidth={2}
stroke={2}
style={{ flexShrink: 0, marginTop: 2 }}
/>
<span>{error}</span>
@ -1271,9 +1271,9 @@ export function FileTranscriptionTab({
style={{ minHeight: 32, padding: "0 12px" }}
>
{copied ? (
<Check size={13} strokeWidth={2.2} />
<IconCheck size={13} stroke={2.2} />
) : (
<Clipboard size={13} strokeWidth={2} />
<IconClipboard size={13} stroke={2} />
)}
{copied ? t("fileTab.result.copied") : t("fileTab.result.copy")}
</button>
@ -1288,7 +1288,7 @@ export function FileTranscriptionTab({
title={summaryAvailable ? undefined : t("summary.unavailable.tooltip")}
style={{ minHeight: 32, padding: "0 12px", gap: 6, opacity: summaryAvailable ? 1 : 0.5, cursor: summaryAvailable ? "pointer" : "not-allowed" }}
>
<ListChecks size={13} strokeWidth={2} />
<IconListCheck size={13} stroke={2} />
{t("summary.button")}
</button>
)}
@ -1305,7 +1305,7 @@ export function FileTranscriptionTab({
style={{ width: 32, minWidth: 32, minHeight: 32, padding: 0 }}
title={t("fileTab.result.clear")}
>
<X size={14} strokeWidth={2} />
<IconX size={14} stroke={2} />
</button>
)}
</div>
@ -1322,7 +1322,7 @@ export function FileTranscriptionTab({
color: "var(--text-mid)",
}}
>
<AlertCircle size={14} strokeWidth={2} style={{ flexShrink: 0, marginTop: 1, color: "var(--accent)" }} />
<IconAlertCircle size={14} stroke={2} style={{ flexShrink: 0, marginTop: 1, color: "var(--accent)" }} />
<span>{t("summary.unavailable.note")}</span>
</div>
)}
@ -1573,9 +1573,9 @@ export function FileTranscriptionTab({
}}
>
{localWhisperDownloaded ? (
<Check size={15} strokeWidth={2.2} />
<IconCheck size={15} stroke={2.2} />
) : (
<AlertCircle size={15} strokeWidth={2} />
<IconAlertCircle size={15} stroke={2} />
)}
<span>
{localWhisperDownloaded
@ -1594,9 +1594,9 @@ export function FileTranscriptionTab({
}}
>
{diarizationInstalled ? (
<Check size={15} strokeWidth={2.2} />
<IconCheck size={15} stroke={2.2} />
) : (
<AlertCircle size={15} strokeWidth={2} />
<IconAlertCircle size={15} stroke={2} />
)}
<span>
{diarizationInstalled
@ -1675,10 +1675,10 @@ export function FileTranscriptionTab({
}}
>
{speakerSetupInstalling ? (
<Loader2
<IconLoader2
className="loading-soft-icon"
size={15}
strokeWidth={2}
stroke={2}
/>
) : null}
{speakerSetupActionLabel}

View file

@ -19,17 +19,17 @@ import {
type SpeakerTranscriptSegment,
} from "../../../lib/store";
import {
AlertCircle,
Check,
Copy,
HelpCircle,
Loader2,
ListChecks,
Pencil,
RotateCcw,
Square,
Trash2,
} from "lucide-react";
IconAlertCircle,
IconCheck,
IconCopy,
IconHelpCircle,
IconLoader2,
IconListCheck,
IconPencil,
IconRotate2,
IconSquare,
IconTrash,
} from "../../../lib/icons";
import {
HISTORY_CLEARED_EVENT,
HISTORY_DELETED_EVENT,
@ -737,7 +737,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
color: hintHelpOpen ? "var(--text-hi)" : "var(--text-low)",
}}
>
<HelpCircle size={16} strokeWidth={2} />
<IconHelpCircle size={16} stroke={2} />
</button>
)}
</div>
@ -854,7 +854,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
: t("mainTab.clearAllTitle")
}
>
<Trash2 size={12} strokeWidth={2} />{" "}
<IconTrash size={12} stroke={2} />{" "}
{isClearArmed ? t("mainTab.confirm") : t("mainTab.clear")}
</button>
)}
@ -1087,10 +1087,10 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
width: "fit-content",
}}
>
<Loader2
<IconLoader2
className="loading-soft-icon"
size={13}
strokeWidth={2}
stroke={2}
/>
<span>{t("mainTab.processing")}</span>
</div>
@ -1113,9 +1113,9 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
width: "fit-content",
}}
>
<AlertCircle
<IconAlertCircle
size={13}
strokeWidth={2}
stroke={2}
/>
<span>
{item.status === "interrupted"
@ -1180,9 +1180,9 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
}}
title={t("mainTab.stopProcessing")}
>
<Square
<IconSquare
size={11}
strokeWidth={2}
stroke={2}
fill="currentColor"
/>
</button>
@ -1210,13 +1210,13 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
title={t("mainTab.retryProcess")}
>
{retryingId === item.id ? (
<Loader2
<IconLoader2
className="loading-soft-icon"
size={12}
strokeWidth={2}
stroke={2}
/>
) : (
<RotateCcw size={12} strokeWidth={2} />
<IconRotate2 size={12} stroke={2} />
)}
</button>
) : (
@ -1228,7 +1228,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
key: "edit",
label: t("rowMenu.edit"),
icon: (
<Pencil size={14} strokeWidth={2} />
<IconPencil size={14} stroke={2} />
),
onSelect: () => editEntry(item),
},
@ -1242,12 +1242,12 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
icon:
copied === item.id ||
retrySucceededId === item.id ? (
<Check
<IconCheck
size={14}
strokeWidth={2.5}
stroke={2.5}
/>
) : (
<Copy size={14} strokeWidth={2} />
<IconCopy size={14} stroke={2} />
),
onSelect: () =>
copyText(item.id, item.cleaned),
@ -1256,9 +1256,9 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
key: "summarize",
label: t("rowMenu.summarize"),
icon: (
<ListChecks
<IconListCheck
size={14}
strokeWidth={2}
stroke={2}
/>
),
disabled: !summaryAvailable,
@ -1283,7 +1283,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) {
}}
title={t("mainTab.delete")}
>
<Trash2 size={12} strokeWidth={2} />
<IconTrash size={12} stroke={2} />
</button>
)}
</div>

View file

@ -2,17 +2,17 @@ import { useEffect, useMemo, useState } from "react";
import type { CSSProperties, ReactElement } from "react";
import { emit, listen } from "@tauri-apps/api/event";
import {
AlertTriangle,
Headphones,
Languages,
Mic,
Play,
Radio,
RefreshCcw,
Speaker,
Square,
Volume2,
} from "lucide-react";
IconAlertTriangle,
IconHeadphones,
IconLanguage,
IconMicrophone,
IconPlayerPlay,
IconBroadcast,
IconRefresh,
IconSpeakerphone,
IconSquare,
IconVolume,
} from "../../../lib/icons";
import {
addHistoryEntry,
@ -691,7 +691,7 @@ export function RealtimeInterpreterTab(): ReactElement {
className="label"
style={{ display: "inline-flex", alignItems: "center", gap: 8 }}
>
<Languages size={14} strokeWidth={2} />
<IconLanguage size={14} stroke={2} />
Beta
</div>
<div
@ -719,7 +719,7 @@ export function RealtimeInterpreterTab(): ReactElement {
>
<Metric label={t("realtime.metric.status")} value={statusLabel(status, t)} />
<Metric label={t("realtime.metric.languages")} value={pairDefinition.label} />
<Metric label={t("realtime.metric.route")} value="Talkis Cloud" />
<Metric label={t("realtime.metric.route")} value="Talkis IconCloud" />
</div>
</div>
@ -733,7 +733,7 @@ export function RealtimeInterpreterTab(): ReactElement {
fontWeight: 700,
}}
>
<Radio size={14} strokeWidth={2.2} />
<IconBroadcast size={14} stroke={2.2} />
{status?.message || t("realtime.statusMessage.off")}
</div>
</div>
@ -831,7 +831,7 @@ export function RealtimeInterpreterTab(): ReactElement {
background: "var(--control-muted)",
}}
>
<span style={{ fontWeight: 700 }}>Cloud proxy</span>
<span style={{ fontWeight: 700 }}>IconCloud proxy</span>
<span
style={{
color: hasCloudToken
@ -868,7 +868,7 @@ export function RealtimeInterpreterTab(): ReactElement {
}}
style={{ width: 16, height: 16, accentColor: "var(--accent)" }}
/>
<Headphones size={16} strokeWidth={2} />
<IconHeadphones size={16} stroke={2} />
<span>{t("realtime.headphonesConfirm")}</span>
</label>
</div>
@ -884,7 +884,7 @@ export function RealtimeInterpreterTab(): ReactElement {
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<Mic size={16} strokeWidth={2} />
<IconMicrophone size={16} stroke={2} />
<div
style={{ fontSize: 15, fontWeight: 700, color: "var(--text-hi)" }}
>
@ -903,7 +903,7 @@ export function RealtimeInterpreterTab(): ReactElement {
}}
style={{ minHeight: 36, borderRadius: 10, padding: "0 12px" }}
>
<RefreshCcw size={14} strokeWidth={2} />
<IconRefresh size={14} stroke={2} />
{t("realtime.button.refresh")}
</button>
<button
@ -920,7 +920,7 @@ export function RealtimeInterpreterTab(): ReactElement {
}}
style={{ minHeight: 36, borderRadius: 10, padding: "0 12px" }}
>
<Volume2 size={14} strokeWidth={2} />
<IconVolume size={14} stroke={2} />
{t("realtime.button.test")}
</button>
{status?.active ? (
@ -934,7 +934,7 @@ export function RealtimeInterpreterTab(): ReactElement {
}}
style={{ minHeight: 36, borderRadius: 10, padding: "0 12px" }}
>
<Square size={13} strokeWidth={2.4} />
<IconSquare size={13} stroke={2.4} />
{t("realtime.button.stop")}
</button>
) : (
@ -948,7 +948,7 @@ export function RealtimeInterpreterTab(): ReactElement {
}}
style={{ minHeight: 36, borderRadius: 10, padding: "0 12px" }}
>
<Play size={13} strokeWidth={2.4} />
<IconPlayerPlay size={13} stroke={2.4} />
{t("realtime.button.start")}
</button>
)}
@ -973,9 +973,9 @@ export function RealtimeInterpreterTab(): ReactElement {
lineHeight: 1.5,
}}
>
<AlertTriangle
<IconAlertTriangle
size={15}
strokeWidth={2}
stroke={2}
style={{ flexShrink: 0 }}
/>
<span>{error}</span>
@ -1026,7 +1026,7 @@ export function RealtimeInterpreterTab(): ReactElement {
>
<div style={{ display: "grid", gap: 10, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Speaker size={15} strokeWidth={2} />
<IconSpeakerphone size={15} stroke={2} />
<div style={{ fontSize: 14, fontWeight: 700 }}>{t("realtime.panel.youToCall")}</div>
</div>
<LevelMeter
@ -1053,7 +1053,7 @@ export function RealtimeInterpreterTab(): ReactElement {
<div style={{ display: "grid", gap: 10, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Headphones size={15} strokeWidth={2} />
<IconHeadphones size={15} stroke={2} />
<div style={{ fontSize: 14, fontWeight: 700 }}>{t("realtime.panel.callToYou")}</div>
</div>
<LevelMeter

View file

@ -4,7 +4,7 @@ import { emit, listen } from "@tauri-apps/api/event";
import { disable as disableAutostart, enable as enableAutostart, isEnabled as isAutostartEnabled } from "@tauri-apps/plugin-autostart";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { openUrl } from "@tauri-apps/plugin-opener";
import { Check, ChevronDown, Mail, Monitor, Moon, Search, Sun, type LucideIcon } from "lucide-react";
import { IconCheck, IconChevronDown, IconMail, IconDeviceDesktop, IconMoon, IconSearch, IconSun, type Icon } from "../../../lib/icons";
import {
getSettings,
@ -56,10 +56,10 @@ const SETTINGS_CARD_STYLE = {
backdropFilter: "none",
WebkitBackdropFilter: "none",
} as const;
const THEME_OPTIONS: Array<{ id: AppSettings["theme"]; Icon: LucideIcon }> = [
{ id: "system", Icon: Monitor },
{ id: "light", Icon: Sun },
{ id: "dark", Icon: Moon },
const THEME_OPTIONS: Array<{ id: AppSettings["theme"]; Icon: Icon }> = [
{ id: "system", Icon: IconDeviceDesktop },
{ id: "light", Icon: IconSun },
{ id: "dark", Icon: IconMoon },
];
export function SettingsTab() {
@ -340,7 +340,7 @@ export function SettingsTab() {
setMicStatus("ready");
setMicMessage(t("settingsGeneralExtra.mic.inUse", { label: activeLabel }));
} catch (err) {
void logError("SETTINGS", `Mic enumeration error: ${err instanceof Error ? err.message : String(err)}`);
void logError("SETTINGS", `IconMicrophone enumeration error: ${err instanceof Error ? err.message : String(err)}`);
setMicStatus("empty");
setMicMessage(t("settingsGeneralExtra.mic.enumFailed"));
}
@ -734,7 +734,7 @@ export function SettingsTab() {
gap: 4,
}}
>
<Icon size={13} strokeWidth={active ? 2.2 : 1.7} style={{ flexShrink: 0 }} />
<Icon size={13} stroke={active ? 2.2 : 1.7} style={{ flexShrink: 0 }} />
<span style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{t(`settings.theme.${id}`)}</span>
</button>
);
@ -754,12 +754,12 @@ export function SettingsTab() {
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{currentLang ? `${currentLang.native} (${currentLang.name})` : settings.language}
</span>
<ChevronDown size={13} strokeWidth={2} style={{ flexShrink: 0, transform: langOpen ? "rotate(180deg)" : "none", transition: "transform 0.15s" }} />
<IconChevronDown size={13} stroke={2} style={{ flexShrink: 0, transform: langOpen ? "rotate(180deg)" : "none", transition: "transform 0.15s" }} />
</button>
{langOpen && (
<div style={{ position: "absolute", top: "calc(100% + 8px)", right: 0, width: 320, maxHeight: 320, background: "var(--dropdown-bg)", border: "1px solid var(--border)", borderRadius: 24, boxShadow: "var(--shadow-panel)", zIndex: 100, display: "flex", flexDirection: "column", overflow: "hidden" }}>
<div style={{ padding: 12, borderBottom: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", gap: 8 }}>
<Search size={13} style={{ color: "var(--text-low)", flexShrink: 0 }} />
<IconSearch size={13} style={{ color: "var(--text-low)", flexShrink: 0 }} />
<input autoFocus value={langSearch} onChange={(e) => setLangSearch(e.target.value)} placeholder={t("settings.recognitionLang.searchPlaceholder")} style={{ border: "none", outline: "none", background: "transparent", fontSize: 12, color: "var(--text-hi)", flex: 1 }} />
</div>
<div style={{ overflow: "auto", flex: 1 }}>
@ -789,7 +789,7 @@ export function SettingsTab() {
<span style={{ minWidth: 28, fontSize: 10, color: "var(--text-low)", fontFamily: "monospace" }}>{lang.code}</span>
<span style={{ flex: 1 }}>{lang.native}</span>
<span style={{ fontSize: 10, color: "var(--text-low)" }}>{lang.name}</span>
{settings.language === lang.code && <Check size={12} strokeWidth={2.5} style={{ color: "var(--text-hi)", flexShrink: 0 }} />}
{settings.language === lang.code && <IconCheck size={12} stroke={2.5} style={{ color: "var(--text-hi)", flexShrink: 0 }} />}
</button>
))}
</div>
@ -817,7 +817,7 @@ export function SettingsTab() {
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{microphones.length === 0 ? t("settings.mic.systemDefault") : visibleMicrophoneLabel}
</span>
<ChevronDown size={13} strokeWidth={2} style={{ flexShrink: 0, transform: micOpen ? "rotate(180deg)" : "none", transition: "transform 0.15s" }} />
<IconChevronDown size={13} stroke={2} style={{ flexShrink: 0, transform: micOpen ? "rotate(180deg)" : "none", transition: "transform 0.15s" }} />
</button>
{micOpen && microphones.length > 0 && (
@ -830,7 +830,7 @@ export function SettingsTab() {
onMouseLeave={(e) => e.currentTarget.style.background = settings.micId === "" ? "var(--dropdown-active)" : "transparent"}
>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{t("settings.mic.systemDefault")}</span>
{settings.micId === "" && <Check size={12} strokeWidth={2.5} style={{ color: "var(--text-hi)", flexShrink: 0 }} />}
{settings.micId === "" && <IconCheck size={12} stroke={2.5} style={{ color: "var(--text-hi)", flexShrink: 0 }} />}
</button>
{microphones.map((m, i) => (
<button
@ -841,7 +841,7 @@ export function SettingsTab() {
onMouseLeave={(e) => e.currentTarget.style.background = settings.micId === m.deviceId ? "var(--dropdown-active)" : "transparent"}
>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{getMicrophoneLabel(m, i)}</span>
{settings.micId === m.deviceId && <Check size={12} strokeWidth={2.5} style={{ color: "var(--text-hi)", flexShrink: 0 }} />}
{settings.micId === m.deviceId && <IconCheck size={12} stroke={2.5} style={{ color: "var(--text-hi)", flexShrink: 0 }} />}
</button>
))}
</div>
@ -1103,7 +1103,7 @@ export function SettingsTab() {
className="btn"
style={{ minHeight: CONTROL_HEIGHT, width: "100%", justifySelf: "end", justifyContent: "center", gap: 8, padding: "0 10px", borderRadius: CONTROL_RADIUS, fontSize: CONTROL_FONT_SIZE }}
>
<Mail size={14} strokeWidth={2} />
<IconMail size={14} stroke={2} />
{t("settings.support.button")}
</button>
</div>

View file

@ -5,29 +5,29 @@ import { emit } from "@tauri-apps/api/event";
import { onOpenUrl } from "@tauri-apps/plugin-deep-link";
import { openUrl } from "@tauri-apps/plugin-opener";
import {
Briefcase,
Check,
Cloud,
Code,
Crown,
Download,
Gauge,
HardDrive,
LogOut,
LucideIcon,
MessageSquare,
Mic,
Pencil,
Plus,
Server,
Sparkles,
Target,
Trash2,
Type,
User,
X,
Zap,
} from "lucide-react";
IconBriefcase,
IconCheck,
IconCloud,
IconCode,
IconCrown,
IconDownload,
IconGauge,
IconDeviceDesktopCog,
IconLogout,
Icon,
IconMessage,
IconMicrophone,
IconPencil,
IconPlus,
IconServer,
IconSparkles,
IconTargetArrow,
IconTrash,
IconTypography,
IconUser,
IconX,
IconBolt,
} from "../../../lib/icons";
import {
AppSettings,
@ -605,7 +605,7 @@ function OptionCard({ active = false, icon, title, description, badge, onClick,
{active && (
<div style={{ position: "absolute", top: 16, right: 16, color: "var(--text-hi)" }}>
<Check size={18} strokeWidth={2.6} />
<IconCheck size={18} stroke={2.6} />
</div>
)}
</div>
@ -645,7 +645,7 @@ function CloudSubscriptionAccountCard({
style={{ width: "100%", height: "100%", borderRadius: "50%", objectFit: "cover" }}
/>
) : (
<User size={22} strokeWidth={1.5} color="var(--text-low)" />
<IconUser size={22} stroke={1.5} color="var(--text-low)" />
)}
</div>
@ -692,7 +692,7 @@ function CloudSubscriptionAccountCard({
}}
title={t("models.account.logout")}
>
<LogOut size={16} strokeWidth={1.8} />
<IconLogout size={16} stroke={1.8} />
</button>
</div>
@ -706,7 +706,7 @@ function SubscriptionPromoContent({ onActivate }: { onActivate: () => void }) {
return (
<>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10, flexWrap: "wrap" }}>
<Crown size={16} strokeWidth={2.2} />
<IconCrown size={16} stroke={2.2} />
<div style={{ display: "flex", alignItems: "baseline", gap: 6, minWidth: 0, flexWrap: "wrap" }}>
<span style={{ fontWeight: 700, fontSize: 14, letterSpacing: "-0.02em", lineHeight: 1.2 }}>{t("models.guest.title")}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: "var(--text-low)", lineHeight: 1.2 }}>{t("models.cta.freeTrial")}</span>
@ -733,7 +733,7 @@ function SubscriptionGuestCard({ onActivate }: { onActivate: () => void }) {
}
/**
* Three-state subscription block, shared by the Models Cloud section and the
* Three-state subscription block, shared by the Models IconCloud section and the
* dedicated "Подписка Talkis" tab: active-subscription banner, signed-in account
* card (activate / log out), or guest promo (sign in + start the free trial).
*/
@ -753,7 +753,7 @@ function SubscriptionCards({
<div className="card" style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16 }}>
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
<div style={{ width: 42, height: 42, borderRadius: 999, background: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center" }}>
<Crown size={20} strokeWidth={2.2} color="var(--accent-contrast)" />
<IconCrown size={20} stroke={2.2} color="var(--accent-contrast)" />
</div>
<div>
<div style={{ fontSize: 16, fontWeight: 700, color: "var(--text-hi)" }}>{t("models.subscription.active")}</div>
@ -1032,7 +1032,7 @@ function PromptLibrary({
<OptionCard
key={preset.id}
active={active}
icon={<MessageSquare size={20} strokeWidth={active ? 2.4 : 1.8} />}
icon={<IconMessage size={20} stroke={active ? 2.4 : 1.8} />}
title={preset.name}
description={<CollapsibleCardText text={preset.prompt} />}
onClick={() => update({ defaultSummaryPromptId: preset.id })}
@ -1067,7 +1067,7 @@ function PromptLibrary({
fontFamily: "var(--font-main)",
}}
>
<Plus size={16} strokeWidth={2} />
<IconPlus size={16} stroke={2} />
{t("models.prompt.create")}
</button>
@ -1090,7 +1090,7 @@ function PromptLibrary({
fontFamily: "var(--font-main)",
}}
>
<Pencil size={15} strokeWidth={2} />
<IconPencil size={15} stroke={2} />
{t("models.common.edit")}
</button>
<button
@ -1111,7 +1111,7 @@ function PromptLibrary({
fontFamily: "var(--font-main)",
}}
>
<Trash2 size={15} strokeWidth={2} />
<IconTrash size={15} stroke={2} />
{t("models.common.delete")}
</button>
</>
@ -1153,7 +1153,7 @@ function TextModelCard({
style={{ width: "100%", border: "none", background: "transparent", padding: "12px 14px", display: "flex", alignItems: "center", gap: 12, cursor: "pointer", textAlign: "left", fontFamily: "var(--font-main)" }}
>
<div style={{ width: 36, height: 36, borderRadius: 999, background: "var(--icon-soft-bg)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
<Sparkles size={18} strokeWidth={1.9} color="var(--text-hi)" />
<IconSparkles size={18} stroke={1.9} color="var(--text-hi)" />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, marginBottom: 3 }}>
@ -1356,7 +1356,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
void syncSettings().catch(() => {});
}, [syncSettings]);
// Cloud profile — always fetch (regardless of tab) so hooks are stable
// IconCloud profile — always fetch (regardless of tab) so hooks are stable
useEffect(() => {
if (getCachedCloudProfile() === undefined) {
loadCloudProfile().catch(() => {});
@ -1602,7 +1602,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
});
};
// Subscription activation / logout — shared by the Models → Cloud section and
// Subscription activation / logout — shared by the Models → IconCloud section and
// the dedicated "Подписка Talkis" tab so both surfaces drive the same flow.
const handleActivateSubscription = async () => {
try {
@ -1657,22 +1657,22 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
const modeOptions: Array<{
id: ModelMode;
label: string;
Icon: LucideIcon;
Icon: Icon;
}> = [
{
id: "cloud",
label: t("models.mode.cloud"),
Icon: Cloud,
Icon: IconCloud,
},
{
id: "api",
label: "API",
Icon: Code,
Icon: IconCode,
},
{
id: "local",
label: t("models.mode.local"),
Icon: Server,
Icon: IconServer,
},
];
@ -2054,7 +2054,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, padding: "10px 12px", borderRadius: 10, background: "var(--control-muted)", border: "1px solid var(--border-subtle)" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5, fontWeight: 650, color: isActive ? "var(--success-bright)" : "var(--text-mid)" }}>
{isActive && <Check size={15} strokeWidth={2.5} />}
{isActive && <IconCheck size={15} stroke={2.5} />}
<span>{isActive ? t("models.modeCommit.active") : t("models.modeCommit.label", { mode: modeLabel })}</span>
</div>
{!isActive && (
@ -2252,9 +2252,9 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
const renderLocalModelStats = (model: LocalModelOption) => {
const speedValueLabel = translateSpeedValue(model.speed);
const accuracyValueLabel = translateAccuracyValue(model.accuracy);
const stats: { key: string; title: string; label: string; Icon: LucideIcon; level: number }[] = [
{ key: "speed", title: t("models.stat.speedTitle", { value: speedValueLabel }), label: t("models.stat.speed"), Icon: Gauge, level: getLocalModelLevel("speed", model.speed) },
{ key: "accuracy", title: t("models.stat.accuracyTitle", { value: accuracyValueLabel }), label: t("models.stat.accuracy"), Icon: Target, level: getLocalModelLevel("accuracy", model.accuracy) },
const stats: { key: string; title: string; label: string; Icon: Icon; level: number }[] = [
{ key: "speed", title: t("models.stat.speedTitle", { value: speedValueLabel }), label: t("models.stat.speed"), Icon: IconGauge, level: getLocalModelLevel("speed", model.speed) },
{ key: "accuracy", title: t("models.stat.accuracyTitle", { value: accuracyValueLabel }), label: t("models.stat.accuracy"), Icon: IconTargetArrow, level: getLocalModelLevel("accuracy", model.accuracy) },
];
const storageLabel = getLocalModelStorageLabel(model);
@ -2265,7 +2265,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
aria-label={t("models.stat.downloadSizeTitle", { value: storageLabel })}
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
<HardDrive size={14} strokeWidth={1.9} color="var(--text-hi)" />
<IconDeviceDesktopCog size={14} stroke={1.9} color="var(--text-hi)" />
<span style={{ fontSize: 12, fontWeight: 650, color: "var(--text-hi)", lineHeight: 1 }}>
{storageLabel}
</span>
@ -2278,7 +2278,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
aria-label={title}
style={{ display: "flex", alignItems: "center", gap: 6 }}
>
<Icon size={14} strokeWidth={1.9} color="var(--text-hi)" />
<Icon size={14} stroke={1.9} color="var(--text-hi)" />
<span style={{ fontSize: 12, fontWeight: 650, color: "var(--text-hi)", lineHeight: 1 }}>
{label}
</span>
@ -2356,7 +2356,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
if (message.includes("отменена")) {
// User cancelled — reset to the not-downloaded state silently.
// IconUser cancelled — reset to the not-downloaded state silently.
setLocalModelActionStates((prev) => {
const next = { ...prev };
delete next[model.id];
@ -2474,7 +2474,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 7,
}}
>
<Icon size={15} strokeWidth={active ? 2.2 : 1.7} />
<Icon size={15} stroke={active ? 2.2 : 1.7} />
<span>{label}</span>
</button>
);
@ -2504,7 +2504,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
{(isCloudSelected || hasActiveSubscription) && (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, flexWrap: "wrap" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, color: isCloudSelected ? "var(--success-bright)" : "var(--text-hi)", fontSize: 12, fontWeight: 600 }}>
<Check size={15} strokeWidth={2.5} />
<IconCheck size={15} stroke={2.5} />
{isCloudSelected ? t("models.connection.usedForRecognition") : t("models.cloud.proReady")}
</div>
@ -2521,7 +2521,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
alignItems: "center",
gap: 8,
}}>
<Check size={14} strokeWidth={2.5} />
<IconCheck size={14} stroke={2.5} />
{t("models.common.selected")}
</div>
) : hasActiveSubscription ? (
@ -2543,7 +2543,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 8,
}}
>
<Check size={14} strokeWidth={2.5} />
<IconCheck size={14} stroke={2.5} />
{t("models.common.select")}
</button>
) : null}
@ -2558,8 +2558,8 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div style={{ display: "flex", background: "var(--control-track)", borderRadius: 10, padding: 3, gap: 2 }}>
{([
{ id: "transcription", label: t("models.local.tabTranscription"), Icon: Mic },
{ id: "text", label: t("models.local.tabText"), Icon: MessageSquare },
{ id: "transcription", label: t("models.local.tabTranscription"), Icon: IconMicrophone },
{ id: "text", label: t("models.local.tabText"), Icon: IconMessage },
] as const).map(({ id, label, Icon }) => {
const active = apiModelKind === id;
@ -2585,7 +2585,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 7,
}}
>
<Icon size={15} strokeWidth={active ? 2.2 : 1.7} />
<Icon size={15} stroke={active ? 2.2 : 1.7} />
<span>{label}</span>
{id === "text" && !apiTextModelConfigured && (
<span
@ -2744,7 +2744,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, flexWrap: "wrap" }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, color: adapterStatus.color, fontSize: 12, fontWeight: 600 }}>
{adapterStatus.status === "success" && <Check size={15} strokeWidth={2.5} />}
{adapterStatus.status === "success" && <IconCheck size={15} stroke={2.5} />}
{adapterStatus.connectionLabel}
</div>
{isAdapterSelected ? (
@ -2760,7 +2760,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
alignItems: "center",
gap: 8,
}}>
<Check size={14} strokeWidth={2.5} />
<IconCheck size={14} stroke={2.5} />
{t("models.common.selected")}
</div>
) : (
@ -2784,7 +2784,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 8,
}}
>
<Check size={14} strokeWidth={2.5} />
<IconCheck size={14} stroke={2.5} />
{t("models.common.select")}
</button>
)}
@ -2814,7 +2814,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
</>
) : (
<>
<Zap size={14} strokeWidth={2.2} />
<IconBolt size={14} stroke={2.2} />
{adapter.testable ? t("models.test.testAndSave") : t("models.test.saveButton")}
</>
)}
@ -2839,8 +2839,8 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div style={{ display: "flex", background: "var(--control-track)", borderRadius: 10, padding: 3, gap: 2 }}>
{([
{ id: "transcription", label: t("models.local.tabTranscription"), Icon: Mic },
{ id: "text", label: t("models.local.tabText"), Icon: MessageSquare },
{ id: "transcription", label: t("models.local.tabTranscription"), Icon: IconMicrophone },
{ id: "text", label: t("models.local.tabText"), Icon: IconMessage },
] as const).map(({ id, label, Icon }) => {
const active = localModelKind === id;
@ -2866,7 +2866,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 7,
}}
>
<Icon size={15} strokeWidth={active ? 2.2 : 1.7} />
<Icon size={15} stroke={active ? 2.2 : 1.7} />
<span>{label}</span>
{id === "text" && !localTextModelSelected && (
<span
@ -3005,7 +3005,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, flexWrap: "wrap" }}>
{modelStatus.connectionLabel && (
<div style={{ display: "flex", alignItems: "center", gap: 8, color: modelStatus.color, fontSize: 12, fontWeight: 600 }}>
{(modelStatus.status === "installed" || modelStatus.status === "selected") && <Check size={15} strokeWidth={2.5} />}
{(modelStatus.status === "installed" || modelStatus.status === "selected") && <IconCheck size={15} stroke={2.5} />}
{modelStatus.connectionLabel}
</div>
)}
@ -3029,7 +3029,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 8,
}}
>
<Check size={14} strokeWidth={2.5} />
<IconCheck size={14} stroke={2.5} />
{t("models.common.select")}
</button>
)}
@ -3062,12 +3062,12 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
>
{isDownloaded ? (
<>
<Trash2 size={14} strokeWidth={2.2} />
<IconTrash size={14} stroke={2.2} />
{t("models.common.delete")}
</>
) : (
<>
<Download size={14} strokeWidth={2.2} />
<IconDownload size={14} stroke={2.2} />
{isRuntimeReady ? t("models.common.download") : t("models.common.unavailable")}
</>
)}
@ -3092,7 +3092,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 8,
}}
>
<X size={14} strokeWidth={2.2} />
<IconX size={14} stroke={2.2} />
{t("models.common.cancel")}
</button>
)}
@ -3189,19 +3189,19 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
);
}
const STYLE_ICONS: Record<AppSettings["style"], LucideIcon> = {
classic: MessageSquare,
business: Briefcase,
tech: Code,
const STYLE_ICONS: Record<AppSettings["style"], Icon> = {
classic: IconMessage,
business: IconBriefcase,
tech: IconCode,
};
const styleTabOptions: Array<{
id: "style" | "prompts";
label: string;
Icon: LucideIcon;
Icon: Icon;
}> = [
{ id: "style", label: t("models.styleTab.style"), Icon: Type },
{ id: "prompts", label: t("models.styleTab.prompts"), Icon: MessageSquare },
{ id: "style", label: t("models.styleTab.style"), Icon: IconTypography },
{ id: "prompts", label: t("models.styleTab.prompts"), Icon: IconMessage },
];
return (
@ -3239,7 +3239,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
gap: 7,
}}
>
<Icon size={15} strokeWidth={active ? 2.2 : 1.7} />
<Icon size={15} stroke={active ? 2.2 : 1.7} />
<span>{label}</span>
</button>
);
@ -3256,7 +3256,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
<OptionCard
key={st.id}
active={isActive}
icon={<Icon size={20} strokeWidth={isActive ? 2.4 : 1.8} />}
icon={<Icon size={20} stroke={isActive ? 2.4 : 1.8} />}
title={st.title}
description={st.description}
onClick={() =>

View file

@ -5,7 +5,7 @@ import { listen } from "@tauri-apps/api/event";
import { getCurrentWebview } from "@tauri-apps/api/webview";
import { cursorPosition, getCurrentWindow } from "@tauri-apps/api/window";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { Check, Copy, FileAudio, Loader2, PhoneCall } from "lucide-react";
import { IconCheck, IconCopy, IconFileMusic, IconLoader2, IconPhoneCall } from "../../lib/icons";
import {
HISTORY_CLEARED_EVENT,
@ -1292,15 +1292,15 @@ function FileDropPill({
}}
>
{isProcessing ? (
<Loader2
<IconLoader2
className="loading-soft-icon"
size={20}
strokeWidth={2}
stroke={2}
/>
) : isSuccess ? (
<Check size={20} strokeWidth={2.4} />
<IconCheck size={20} stroke={2.4} />
) : (
<FileAudio size={20} strokeWidth={2} />
<IconFileMusic size={20} stroke={2} />
)}
</span>
<span
@ -1401,13 +1401,13 @@ function CallBubble({
}}
>
{isStarting || isProcessing ? (
<Loader2
<IconLoader2
className="loading-soft-icon"
size={iconSize}
strokeWidth={2.2}
stroke={2.2}
/>
) : isSuccess ? (
<Check size={iconSize} strokeWidth={2.6} />
<IconCheck size={iconSize} stroke={2.6} />
) : isError ? (
<span
style={{
@ -1419,7 +1419,7 @@ function CallBubble({
!
</span>
) : (
<PhoneCall size={iconSize} strokeWidth={isRecording ? 2.4 : 2} />
<IconPhoneCall size={iconSize} stroke={isRecording ? 2.4 : 2} />
)}
</div>
</ActiveWidgetShell>
@ -1627,9 +1627,9 @@ function IdlePill({
}}
>
{copySucceeded ? (
<Check size={12} strokeWidth={2.4} />
<IconCheck size={12} stroke={2.4} />
) : (
<Copy size={12} strokeWidth={2} />
<IconCopy size={12} stroke={2} />
)}
</button>
)}
@ -1783,8 +1783,8 @@ function FlowRecordingWidget({
)}
{state === "long" && (
<span className={`flow-widget-long-mark is-${longMark}`}>
{longMark === "phone" && <PhoneCall size={9} strokeWidth={2.2} />}
{longMark === "success" && <Check size={9} strokeWidth={2.6} />}
{longMark === "phone" && <IconPhoneCall size={9} stroke={2.2} />}
{longMark === "success" && <IconCheck size={9} stroke={2.6} />}
{longMark === "error" && "!"}
</span>
)}

View file

@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react";
import type { ReactElement } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { AlertCircle, Info } from "lucide-react";
import { IconAlertCircle, IconInfoCircle } from "../../lib/icons";
import { NOTICE_AREA_HEIGHT, NOTICE_WIDGET_WIDTH, WIDGET_NOTICE_EVENT, type WidgetNoticeState } from "./widgetConstants";
@ -50,7 +50,7 @@ export function WidgetNoticeOverlay(): ReactElement | null {
const toggleExpanded = () => {
setExpanded((value) => !value);
};
const Icon = notice.tone === "error" ? AlertCircle : Info;
const Icon = notice.tone === "error" ? IconAlertCircle : IconInfoCircle;
const iconColor = notice.tone === "error" ? "rgba(184,52,52,0.9)" : "rgba(0,0,0,0.58)";
return (
@ -113,7 +113,7 @@ export function WidgetNoticeOverlay(): ReactElement | null {
>
<Icon
size={15}
strokeWidth={2.1}
stroke={2.1}
aria-hidden="true"
style={{ flexShrink: 0, color: iconColor }}
/>