mirror of
https://github.com/SerTimBerrners-Lee/talkis.git
synced 2026-07-09 17:29:15 +00:00
Release v0.3.6
This commit is contained in:
parent
b83ea7254f
commit
2f0bc345c5
24 changed files with 572 additions and 261 deletions
|
|
@ -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.
|
||||
|
|
|
|||
7
bun.lock
7
bun.lock
|
|
@ -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=="],
|
||||
|
|
|
|||
61
docs/release/review-v0.3.6.md
Normal file
61
docs/release/review-v0.3.6.md
Normal 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.
|
||||
10
package.json
10
package.json
|
|
@ -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
56
scripts/run-vite.mjs
Normal 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
2
src-tauri/Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
180
src/lib/icons.ts
Normal 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,
|
||||
};
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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={() =>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue