feat: wire model download UI and native docs
This commit is contained in:
1 parent
0c39063150
commit
8f754b3f00
8 files changed
+117
-51
No files matched your search
@@ -26,7 +26,7 @@ jobs:
|
|||||||
if: matrix.platform == 'ubuntu-22.04'
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libasound2-dev
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Install Tauri Linux dependencies
|
- name: Install Tauri Linux dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libasound2-dev
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ name: Release
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*.*.*'
|
- "v*.*.*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: 'Tag de release, por exemplo v0.1.0'
|
description: "Tag de release, por exemplo v0.1.0"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
@@ -20,9 +20,9 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- platform: windows-latest
|
- platform: windows-latest
|
||||||
args: ''
|
args: ""
|
||||||
- platform: ubuntu-22.04
|
- platform: ubuntu-22.04
|
||||||
args: ''
|
args: ""
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
if: matrix.platform == 'ubuntu-22.04'
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libasound2-dev
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tagName: ${{ github.event.inputs.tag || github.ref_name }}
|
tagName: ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
releaseName: Transcript ${{ github.event.inputs.tag || github.ref_name }}
|
releaseName: Transcript ${{ github.event.inputs.tag || github.ref_name }}
|
||||||
releaseBody: 'Build automatizado do Transcript para Windows e Linux.'
|
releaseBody: "Build automatizado do Transcript para Windows e Linux."
|
||||||
releaseDraft: false
|
releaseDraft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
args: ${{ matrix.args }}
|
args: ${{ matrix.args }}
|
||||||
@@ -4,26 +4,28 @@ Aplicativo desktop de transcrição para Windows e Linux, construído com Tauri
|
|||||||
|
|
||||||
## Estado atual
|
## Estado atual
|
||||||
|
|
||||||
Este repositório contém o MVP estrutural:
|
Este repositório contém um MVP nativo funcional:
|
||||||
|
|
||||||
- app Tauri 2 funcional;
|
- app Tauri 2 funcional;
|
||||||
- modal editável em TypeScript sem framework pesado;
|
- modal editável em TypeScript sem framework pesado;
|
||||||
- backend Rust modular;
|
- backend Rust modular;
|
||||||
- catálogo do modelo `cstr/parakeet-tdt-0.6b-v3-GGUF` em Q8;
|
- catálogo e download do modelo `cstr/parakeet-tdt-0.6b-v3-GGUF` em Q8;
|
||||||
- contratos para CrispASR sidecar, API OpenAI-compatible, gravação e inserção de texto;
|
- captura real de microfone em WAV via CPAL;
|
||||||
|
- execução do sidecar CrispASR/Parakeet;
|
||||||
|
- hotkey global `Ctrl+Shift+Space`;
|
||||||
|
- inserção híbrida por clipboard + paste automático no Windows/X11 quando disponível;
|
||||||
|
- fallback manual no Wayland;
|
||||||
- tray/system tray básico;
|
- tray/system tray básico;
|
||||||
- CI, build e release via GitHub Actions.
|
- CI, build e release via GitHub Actions.
|
||||||
|
|
||||||
Ainda não implementa captura real de áudio, hotkey global, download real do modelo ou injeção nativa em outros aplicativos. Essas integrações já têm módulos dedicados para implementação incremental.
|
|
||||||
|
|
||||||
## Arquitetura
|
## Arquitetura
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/ Frontend TypeScript/CSS
|
src/ Frontend TypeScript/CSS
|
||||||
src-tauri/src/
|
src-tauri/src/
|
||||||
audio/ Estado de gravação MVP
|
audio/ Captura de microfone e gravação WAV
|
||||||
transcription/ Contratos, CrispASR sidecar e API compatível com OpenAI
|
transcription/ Contratos, CrispASR sidecar e API compatível com OpenAI
|
||||||
injection/ Contratos/fallback de inserção
|
injection/ Clipboard, paste automático e fallback manual
|
||||||
models/ Catálogo do modelo offline
|
models/ Catálogo do modelo offline
|
||||||
settings/ Configuração padrão
|
settings/ Configuração padrão
|
||||||
tray.rs Menu da bandeja do sistema
|
tray.rs Menu da bandeja do sistema
|
||||||
@@ -44,10 +46,22 @@ A regra de design é manter arquivos pequenos, responsabilidades claras e zero o
|
|||||||
|
|
||||||
O modelo não deve ser embutido no instalador principal. A estratégia recomendada é instalar o app leve e baixar o GGUF pelo gerenciador de modelos.
|
O modelo não deve ser embutido no instalador principal. A estratégia recomendada é instalar o app leve e baixar o GGUF pelo gerenciador de modelos.
|
||||||
|
|
||||||
|
## Sidecar CrispASR
|
||||||
|
|
||||||
|
O app chama por padrão o binário `parakeet-main` com:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
parakeet-main -m <modelo.gguf> -f <audio.wav> -t <threads>
|
||||||
|
```
|
||||||
|
|
||||||
|
Para o modelo v3, o projeto CrispASR recomenda compilar o target `parakeet-main` no branch `parakeet`. O app limita automaticamente o número de threads a no máximo 4 para manter compatibilidade com PCs simples.
|
||||||
|
|
||||||
|
Enquanto o binário não estiver no `PATH` ou empacotado como sidecar, a transcrição local retorna erro claro após a gravação.
|
||||||
|
|
||||||
## Compatibilidade Windows/Linux
|
## Compatibilidade Windows/Linux
|
||||||
|
|
||||||
- Windows: alvo principal para inserção automática futura via APIs Win32.
|
- Windows: clipboard + tentativa de `Ctrl+V` via `SendKeys`.
|
||||||
- Linux X11: alvo de automação completa quando possível.
|
- Linux X11: clipboard + tentativa de `Ctrl+V` via `xdotool` quando instalado.
|
||||||
- Linux Wayland: suporte parcial por limitação de segurança dos compositors. O fallback seguro é copiar o texto e orientar o usuário a colar manualmente.
|
- Linux Wayland: suporte parcial por limitação de segurança dos compositors. O fallback seguro é copiar o texto e orientar o usuário a colar manualmente.
|
||||||
|
|
||||||
## Comandos locais
|
## Comandos locais
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
- Modify: `src-tauri/src/ui_commands.rs`
|
- Modify: `src-tauri/src/ui_commands.rs`
|
||||||
- Modify: `src-tauri/Cargo.toml`
|
- Modify: `src-tauri/Cargo.toml`
|
||||||
|
|
||||||
- [ ] Add URL de download do Hugging Face e resolução de diretório local.
|
- [x] Add URL de download do Hugging Face e resolução de diretório local.
|
||||||
- [ ] Add validação por existência e tamanho mínimo/esperado.
|
- [x] Add validação por existência e tamanho mínimo/esperado.
|
||||||
- [ ] Add comando `download_default_model` com escrita em arquivo temporário e rename atômico.
|
- [x] Add comando `download_default_model` com escrita em arquivo temporário e rename atômico.
|
||||||
- [ ] Testar status notInstalled/ready sem baixar o arquivo real.
|
- [x] Testar status notInstalled/ready sem baixar o arquivo real.
|
||||||
|
|
||||||
## Task 2: Captura real de áudio
|
## Task 2: Captura real de áudio
|
||||||
|
|
||||||
@@ -30,10 +30,10 @@
|
|||||||
- Modify: `src-tauri/src/ui_commands.rs`
|
- Modify: `src-tauri/src/ui_commands.rs`
|
||||||
- Modify: `src-tauri/Cargo.toml`
|
- Modify: `src-tauri/Cargo.toml`
|
||||||
|
|
||||||
- [ ] Implementar gravador CPAL que escreve WAV via hound.
|
- [x] Implementar gravador CPAL que escreve WAV via hound.
|
||||||
- [ ] Guardar stream ativo no estado para manter gravação viva.
|
- [x] Manter stream vivo em thread de gravação e guardar handles seguros no estado.
|
||||||
- [ ] Stop finaliza WAV e retorna caminho.
|
- [x] Stop finaliza WAV e retorna caminho.
|
||||||
- [ ] Manter teste unitário de máquina de estados.
|
- [x] Manter teste unitário de máquina de estados.
|
||||||
|
|
||||||
## Task 3: Execução CrispASR
|
## Task 3: Execução CrispASR
|
||||||
|
|
||||||
@@ -42,10 +42,10 @@
|
|||||||
- Modify: `src-tauri/src/ui_commands.rs`
|
- Modify: `src-tauri/src/ui_commands.rs`
|
||||||
- Modify: `src-tauri/tauri.conf.json`
|
- Modify: `src-tauri/tauri.conf.json`
|
||||||
|
|
||||||
- [ ] Resolver binário via variável/env/config e depois sidecar empacotado.
|
- [x] Resolver binário padrão `parakeet-main` para o modelo v3.
|
||||||
- [ ] Executar `crispasr -m <model> -f <audio>`.
|
- [x] Executar `parakeet-main -m <model> -f <audio> -t <threads>`.
|
||||||
- [ ] Capturar stdout/stderr e retornar erro claro.
|
- [x] Capturar stdout/stderr e retornar erro claro.
|
||||||
- [ ] Parsear texto simples ou JSON quando disponível.
|
- [x] Parsear stdout como texto simples; JSON fica para evolução futura se o sidecar for configurado para isso.
|
||||||
|
|
||||||
## Task 4: Hotkey global
|
## Task 4: Hotkey global
|
||||||
|
|
||||||
@@ -53,9 +53,9 @@
|
|||||||
- Modify: `src-tauri/Cargo.toml`
|
- Modify: `src-tauri/Cargo.toml`
|
||||||
- Modify: `src-tauri/src/lib.rs`
|
- Modify: `src-tauri/src/lib.rs`
|
||||||
|
|
||||||
- [ ] Adicionar `tauri-plugin-global-shortcut`.
|
- [x] Adicionar `tauri-plugin-global-shortcut`.
|
||||||
- [ ] Registrar `Ctrl+Alt+Space` no setup.
|
- [x] Registrar `Ctrl+Shift+Space` no setup.
|
||||||
- [ ] Ao pressionar, mostrar/focar janela e emitir evento `hotkey-toggle-recording` para a UI.
|
- [x] Ao pressionar, emitir evento `transcript://toggle-recording` para a UI.
|
||||||
|
|
||||||
## Task 5: Inserção/cópia cross-platform
|
## Task 5: Inserção/cópia cross-platform
|
||||||
|
|
||||||
@@ -64,10 +64,10 @@
|
|||||||
- Modify: `src-tauri/src/ui_commands.rs`
|
- Modify: `src-tauri/src/ui_commands.rs`
|
||||||
- Modify: `src-tauri/Cargo.toml`
|
- Modify: `src-tauri/Cargo.toml`
|
||||||
|
|
||||||
- [ ] Adicionar clipboard backend com `arboard`.
|
- [x] Adicionar clipboard backend com `arboard`.
|
||||||
- [ ] Implementar `request_insert_text` que sempre coloca o texto no clipboard.
|
- [x] Implementar `request_insert_text` que sempre coloca o texto no clipboard.
|
||||||
- [ ] Retornar mensagens por plataforma/display server.
|
- [x] Retornar mensagens por plataforma/display server.
|
||||||
- [ ] Manter Wayland como parcial/manual.
|
- [x] Manter Wayland como parcial/manual.
|
||||||
|
|
||||||
## Task 6: UI, CI e docs
|
## Task 6: UI, CI e docs
|
||||||
|
|
||||||
@@ -77,10 +77,10 @@
|
|||||||
- Modify: `.github/workflows/*.yml`
|
- Modify: `.github/workflows/*.yml`
|
||||||
- Modify: `README.md`
|
- Modify: `README.md`
|
||||||
|
|
||||||
- [ ] Mostrar status de modelo e botão de download.
|
- [x] Mostrar status de modelo e botão de download.
|
||||||
- [ ] Reagir ao evento de hotkey.
|
- [x] Reagir ao evento de hotkey.
|
||||||
- [ ] Atualizar CI Linux com `libasound2-dev`.
|
- [x] Atualizar CI Linux com `libasound2-dev`.
|
||||||
- [ ] Rodar verificação final completa.
|
- [x] Rodar verificação final completa.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ impl TextInjector for FallbackInjector {
|
|||||||
fn current_display_server() -> DisplayServer {
|
fn current_display_server() -> DisplayServer {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
return DisplayServer::Windows;
|
DisplayServer::Windows
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -127,15 +127,19 @@ fn current_display_server() -> DisplayServer {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
if session_type == "wayland" || std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
if session_type == "wayland" || std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
||||||
return DisplayServer::Wayland;
|
DisplayServer::Wayland
|
||||||
}
|
} else if session_type == "x11" || std::env::var_os("DISPLAY").is_some() {
|
||||||
if session_type == "x11" || std::env::var_os("DISPLAY").is_some() {
|
DisplayServer::X11
|
||||||
return DisplayServer::X11;
|
} else {
|
||||||
|
DisplayServer::Unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||||
|
{
|
||||||
DisplayServer::Unknown
|
DisplayServer::Unknown
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_to_clipboard(text: &str) -> Result<String, String> {
|
fn copy_to_clipboard(text: &str) -> Result<String, String> {
|
||||||
let mut clipboard = Clipboard::new().map_err(|error| format!("Falha ao acessar clipboard: {error}"))?;
|
let mut clipboard = Clipboard::new().map_err(|error| format!("Falha ao acessar clipboard: {error}"))?;
|
||||||
|
|||||||
+37
-2
@@ -33,10 +33,12 @@ export interface ModelWithStatus {
|
|||||||
filename: string;
|
filename: string;
|
||||||
quantization: string;
|
quantization: string;
|
||||||
sizeBytes: number;
|
sizeBytes: number;
|
||||||
|
downloadUrl?: string;
|
||||||
languages: string[];
|
languages: string[];
|
||||||
recommendedForSimplePc: boolean;
|
recommendedForSimplePc: boolean;
|
||||||
};
|
};
|
||||||
status: "notInstalled" | "ready" | { error: string };
|
status: "notInstalled" | "ready" | { error: string };
|
||||||
|
localPath?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TranscriptDraft {
|
export interface TranscriptDraft {
|
||||||
@@ -123,6 +125,12 @@ function formatBytes(bytes: number): string {
|
|||||||
return `${mib.toFixed(0)} MiB`;
|
return `${mib.toFixed(0)} MiB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function modelStatusLabel(status: ModelWithStatus["status"]): string {
|
||||||
|
if (status === "ready") return "pronto";
|
||||||
|
if (status === "notInstalled") return "não instalado";
|
||||||
|
return status.error;
|
||||||
|
}
|
||||||
|
|
||||||
async function safeSnapshot(client: NativeClient): Promise<AppSnapshot> {
|
async function safeSnapshot(client: NativeClient): Promise<AppSnapshot> {
|
||||||
try {
|
try {
|
||||||
return await client.invoke<AppSnapshot>("get_app_snapshot");
|
return await client.invoke<AppSnapshot>("get_app_snapshot");
|
||||||
@@ -205,7 +213,13 @@ export function createTranscriptionApp(
|
|||||||
const modelPanel = element("aside", "model-panel");
|
const modelPanel = element("aside", "model-panel");
|
||||||
const modelTitle = element("h2", undefined, "Modelo offline");
|
const modelTitle = element("h2", undefined, "Modelo offline");
|
||||||
const modelDetails = element("p");
|
const modelDetails = element("p");
|
||||||
modelPanel.append(modelTitle, modelDetails);
|
const modelStatus = element("p", "model-status");
|
||||||
|
const downloadModelButton = element(
|
||||||
|
"button",
|
||||||
|
"ghost-button",
|
||||||
|
"Baixar modelo",
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
modelPanel.append(modelTitle, modelDetails, modelStatus, downloadModelButton);
|
||||||
|
|
||||||
shell.append(topbar, editorLabel, editor, message, actions, modelPanel);
|
shell.append(topbar, editorLabel, editor, message, actions, modelPanel);
|
||||||
mount.append(shell);
|
mount.append(shell);
|
||||||
@@ -223,10 +237,17 @@ export function createTranscriptionApp(
|
|||||||
? "Parar e transcrever"
|
? "Parar e transcrever"
|
||||||
: "Iniciar gravação";
|
: "Iniciar gravação";
|
||||||
|
|
||||||
const firstModel = snapshot.models[0]?.model;
|
const firstModelStatus = snapshot.models[0];
|
||||||
|
const firstModel = firstModelStatus?.model;
|
||||||
modelDetails.textContent = firstModel
|
modelDetails.textContent = firstModel
|
||||||
? `${firstModel.displayName} · ${firstModel.quantization} · ${formatBytes(firstModel.sizeBytes)} · ${firstModel.repository}`
|
? `${firstModel.displayName} · ${firstModel.quantization} · ${formatBytes(firstModel.sizeBytes)} · ${firstModel.repository}`
|
||||||
: "Nenhum modelo offline homologado encontrado.";
|
: "Nenhum modelo offline homologado encontrado.";
|
||||||
|
modelStatus.textContent = firstModelStatus
|
||||||
|
? `Status: ${modelStatusLabel(firstModelStatus.status)}`
|
||||||
|
: "Status: indisponível";
|
||||||
|
downloadModelButton.disabled = firstModelStatus?.status === "ready";
|
||||||
|
downloadModelButton.textContent =
|
||||||
|
firstModelStatus?.status === "ready" ? "Modelo pronto" : "Baixar modelo";
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = async (): Promise<void> => {
|
const refresh = async (): Promise<void> => {
|
||||||
@@ -249,6 +270,20 @@ export function createTranscriptionApp(
|
|||||||
void toggleRecording().catch(showError);
|
void toggleRecording().catch(showError);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
downloadModelButton.addEventListener("click", () => {
|
||||||
|
downloadModelButton.disabled = true;
|
||||||
|
downloadModelButton.textContent = "Baixando...";
|
||||||
|
message.textContent = "Baixando modelo offline. Isso pode demorar.";
|
||||||
|
void client
|
||||||
|
.invoke("download_default_model_command")
|
||||||
|
.then(refresh)
|
||||||
|
.catch((error) => {
|
||||||
|
downloadModelButton.disabled = false;
|
||||||
|
downloadModelButton.textContent = "Baixar modelo";
|
||||||
|
showError(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
continueButton.addEventListener("click", () => {
|
continueButton.addEventListener("click", () => {
|
||||||
void client
|
void client
|
||||||
.invoke<AppSnapshot>("start_recording")
|
.invoke<AppSnapshot>("start_recording")
|
||||||
|
|||||||
+17
-4
@@ -2,8 +2,8 @@
|
|||||||
color: #15120f;
|
color: #15120f;
|
||||||
background: #f4efe7;
|
background: #f4efe7;
|
||||||
font-family:
|
font-family:
|
||||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
sans-serif;
|
"Segoe UI", sans-serif;
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,11 @@ body {
|
|||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 15% 10%, rgba(122, 79, 56, 0.18), transparent 24rem),
|
radial-gradient(
|
||||||
|
circle at 15% 10%,
|
||||||
|
rgba(122, 79, 56, 0.18),
|
||||||
|
transparent 24rem
|
||||||
|
),
|
||||||
linear-gradient(135deg, #f7f1e8 0%, #ece4da 100%);
|
linear-gradient(135deg, #f7f1e8 0%, #ece4da 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,9 +171,14 @@ h1 {
|
|||||||
border: 1px solid rgba(106, 74, 54, 0.22);
|
border: 1px solid rgba(106, 74, 54, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
.model-panel {
|
.model-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.35rem;
|
gap: 0.55rem;
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
border-radius: 0.9rem;
|
border-radius: 0.9rem;
|
||||||
background: rgba(234, 223, 210, 0.46);
|
background: rgba(234, 223, 210, 0.46);
|
||||||
@@ -179,6 +188,10 @@ h1 {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.model-status {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
#app {
|
#app {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|||||||
Reference in new issue
Block a user