mirror of
https://github.com/block/goose.git
synced 2026-07-09 16:09:22 +00:00
feat (ui): Migrate dictation local model manager to ACP (#10131)
This commit is contained in:
parent
31bc265a69
commit
2f32070975
7 changed files with 70 additions and 28 deletions
|
|
@ -2145,6 +2145,7 @@ pub struct DictationLocalModelStatus {
|
|||
pub size_mb: u32,
|
||||
pub downloaded: bool,
|
||||
pub download_in_progress: bool,
|
||||
pub recommended: bool,
|
||||
}
|
||||
|
||||
/// Kick off a background download of a local Whisper model.
|
||||
|
|
|
|||
|
|
@ -5548,6 +5548,9 @@
|
|||
},
|
||||
"downloadInProgress": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"recommended": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -5556,7 +5559,8 @@
|
|||
"description",
|
||||
"sizeMb",
|
||||
"downloaded",
|
||||
"downloadInProgress"
|
||||
"downloadInProgress",
|
||||
"recommended"
|
||||
]
|
||||
},
|
||||
"DictationModelDownloadRequest_unstable": {
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ impl GooseAcpAgent {
|
|||
use crate::download_manager::{get_download_manager, DownloadStatus};
|
||||
|
||||
let manager = get_download_manager();
|
||||
let recommended_id = whisper::recommend_model();
|
||||
let models = whisper::available_models()
|
||||
.iter()
|
||||
.map(|model| DictationLocalModelStatus {
|
||||
|
|
@ -172,6 +173,7 @@ impl GooseAcpAgent {
|
|||
.get_progress(model.id)
|
||||
.map(|progress| progress.status == DownloadStatus::Downloading)
|
||||
.unwrap_or(false),
|
||||
recommended: model.id == recommended_id,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import type { DictationProviderStatusEntry } from '@aaif/goose-sdk';
|
||||
import type {
|
||||
DictationDownloadProgress,
|
||||
DictationLocalModelStatus,
|
||||
DictationProviderStatusEntry,
|
||||
} from '@aaif/goose-sdk';
|
||||
import { getAcpClient } from './acpConnection';
|
||||
|
||||
export type { DictationProviderStatusEntry };
|
||||
|
||||
export type DictationProviders = Record<string, DictationProviderStatusEntry>;
|
||||
export type LocalDictationModel = DictationLocalModelStatus;
|
||||
export type LocalDictationDownloadProgress = DictationDownloadProgress;
|
||||
|
||||
export async function getDictationConfig(): Promise<DictationProviders> {
|
||||
const client = await getAcpClient();
|
||||
|
|
@ -20,3 +26,32 @@ export async function transcribeDictation(
|
|||
const response = await client.goose.dictationTranscribe_unstable({ audio, mimeType, provider });
|
||||
return response.text;
|
||||
}
|
||||
|
||||
export async function listLocalDictationModels(): Promise<LocalDictationModel[]> {
|
||||
const client = await getAcpClient();
|
||||
const response = await client.goose.dictationModelsList_unstable({});
|
||||
return response.models;
|
||||
}
|
||||
|
||||
export async function downloadLocalDictationModel(modelId: string): Promise<void> {
|
||||
const client = await getAcpClient();
|
||||
await client.goose.dictationModelsDownload_unstable({ modelId });
|
||||
}
|
||||
|
||||
export async function getLocalDictationModelDownloadProgress(
|
||||
modelId: string
|
||||
): Promise<LocalDictationDownloadProgress | null> {
|
||||
const client = await getAcpClient();
|
||||
const response = await client.goose.dictationModelsDownloadProgress_unstable({ modelId });
|
||||
return response.progress ?? null;
|
||||
}
|
||||
|
||||
export async function cancelLocalDictationModelDownload(modelId: string): Promise<void> {
|
||||
const client = await getAcpClient();
|
||||
await client.goose.dictationModelsCancel_unstable({ modelId });
|
||||
}
|
||||
|
||||
export async function deleteLocalDictationModel(modelId: string): Promise<void> {
|
||||
const client = await getAcpClient();
|
||||
await client.goose.dictationModelsDelete_unstable({ modelId });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ import { Download, Trash2, X, Check, ChevronDown, ChevronUp } from 'lucide-react
|
|||
import { Button } from '../../ui/button';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
import {
|
||||
listModels,
|
||||
downloadModel,
|
||||
getDownloadProgress,
|
||||
cancelDownload as cancelDownloadApi,
|
||||
deleteModel as deleteModelApi,
|
||||
type WhisperModelResponse,
|
||||
type DownloadProgress,
|
||||
} from '../../../api';
|
||||
cancelLocalDictationModelDownload,
|
||||
deleteLocalDictationModel,
|
||||
downloadLocalDictationModel,
|
||||
getLocalDictationModelDownloadProgress,
|
||||
listLocalDictationModels,
|
||||
type LocalDictationDownloadProgress,
|
||||
type LocalDictationModel,
|
||||
} from '../../../acp/dictation';
|
||||
import { defineMessages, useIntl } from '../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
|
|
@ -72,8 +72,10 @@ const capitalize = (str: string): string => {
|
|||
|
||||
export const LocalModelManager = () => {
|
||||
const intl = useIntl();
|
||||
const [models, setModels] = useState<WhisperModelResponse[]>([]);
|
||||
const [downloads, setDownloads] = useState<Map<string, DownloadProgress>>(new Map());
|
||||
const [models, setModels] = useState<LocalDictationModel[]>([]);
|
||||
const [downloads, setDownloads] = useState<Map<string, LocalDictationDownloadProgress>>(
|
||||
new Map()
|
||||
);
|
||||
const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
|
||||
const [showAllModels, setShowAllModels] = useState(false);
|
||||
const { read, upsert } = useConfig();
|
||||
|
|
@ -105,10 +107,8 @@ export const LocalModelManager = () => {
|
|||
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const response = await listModels();
|
||||
if (response.data) {
|
||||
setModels(response.data);
|
||||
}
|
||||
const models = await listLocalDictationModels();
|
||||
setModels(models);
|
||||
} catch (error) {
|
||||
console.error('Failed to load models:', error);
|
||||
}
|
||||
|
|
@ -116,7 +116,7 @@ export const LocalModelManager = () => {
|
|||
|
||||
const startDownload = async (modelId: string) => {
|
||||
try {
|
||||
await downloadModel({ path: { model_id: modelId } });
|
||||
await downloadLocalDictationModel(modelId);
|
||||
pollDownloadProgress(modelId);
|
||||
} catch (error) {
|
||||
console.error('Failed to start download:', error);
|
||||
|
|
@ -126,9 +126,8 @@ export const LocalModelManager = () => {
|
|||
const pollDownloadProgress = (modelId: string) => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
const response = await getDownloadProgress({ path: { model_id: modelId } });
|
||||
if (response.data) {
|
||||
const progress = response.data;
|
||||
const progress = await getLocalDictationModelDownloadProgress(modelId);
|
||||
if (progress) {
|
||||
setDownloads((prev) => new Map(prev).set(modelId, progress));
|
||||
|
||||
if (progress.status === 'completed') {
|
||||
|
|
@ -151,7 +150,7 @@ export const LocalModelManager = () => {
|
|||
|
||||
const cancelDownload = async (modelId: string) => {
|
||||
try {
|
||||
await cancelDownloadApi({ path: { model_id: modelId } });
|
||||
await cancelLocalDictationModelDownload(modelId);
|
||||
setDownloads((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.delete(modelId);
|
||||
|
|
@ -167,7 +166,7 @@ export const LocalModelManager = () => {
|
|||
if (!window.confirm(intl.formatMessage(i18n.deleteConfirm))) return;
|
||||
|
||||
try {
|
||||
await deleteModelApi({ path: { model_id: modelId } });
|
||||
await deleteLocalDictationModel(modelId);
|
||||
if (selectedModelId === modelId) {
|
||||
await upsert(LOCAL_WHISPER_MODEL_CONFIG_KEY, '', false);
|
||||
setSelectedModelId(null);
|
||||
|
|
@ -224,7 +223,7 @@ export const LocalModelManager = () => {
|
|||
<h4 className="text-sm font-medium text-text-primary">
|
||||
{capitalize(model.id)}
|
||||
</h4>
|
||||
<span className="text-xs text-text-secondary">{model.size_mb}MB</span>
|
||||
<span className="text-xs text-text-secondary">{model.sizeMb}MB</span>
|
||||
{model.recommended && (
|
||||
<span className="text-xs bg-blue-500 text-white px-2 py-0.5 rounded">
|
||||
{intl.formatMessage(i18n.recommended)}
|
||||
|
|
@ -264,7 +263,7 @@ export const LocalModelManager = () => {
|
|||
) : isDownloading ? (
|
||||
<>
|
||||
<div className="text-xs text-text-secondary min-w-[60px]">
|
||||
{progress.progress_percent.toFixed(0)}%
|
||||
{progress.progressPercent.toFixed(0)}%
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => cancelDownload(model.id)}>
|
||||
<X className="w-4 h-4" />
|
||||
|
|
@ -284,14 +283,13 @@ export const LocalModelManager = () => {
|
|||
<div className="w-full bg-background-secondary rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-background-inverse h-1.5 rounded-full transition-all"
|
||||
style={{ width: `${progress.progress_percent}%` }}
|
||||
style={{ width: `${progress.progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-text-secondary">
|
||||
<span>
|
||||
{formatBytes(progress.bytes_downloaded)} / {formatBytes(progress.total_bytes)}
|
||||
{formatBytes(progress.bytesDownloaded)} / {formatBytes(progress.totalBytes)}
|
||||
</span>
|
||||
{progress.speed_bps && <span>{formatBytes(progress.speed_bps)}/s</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2256,6 +2256,7 @@ export type DictationLocalModelStatus = {
|
|||
sizeMb: number;
|
||||
downloaded: boolean;
|
||||
downloadInProgress: boolean;
|
||||
recommended: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2308,7 +2308,8 @@ export const zDictationLocalModelStatus = z.object({
|
|||
description: z.string(),
|
||||
sizeMb: z.number().int().gte(0),
|
||||
downloaded: z.boolean(),
|
||||
downloadInProgress: z.boolean()
|
||||
downloadInProgress: z.boolean(),
|
||||
recommended: z.boolean()
|
||||
});
|
||||
|
||||
export const zDictationModelsListResponse_unstable = z.object({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue