feat(desktop): embed BoosAPI Desktop client as a subproject

Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
This commit is contained in:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,58 @@
import React, { useCallback } from 'react';
import { Check, Copy } from 'lucide-react';
import { Button, message, theme } from 'antd';
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard';
export interface CopyButtonProps {
/** Text to copy, or async function returning text to copy */
text: string | (() => string | Promise<string>);
/** Icon size in px (default: 14) */
size?: number;
/** Duration of success state in ms (default: 2000) */
timeout?: number;
/** If provided, shows message.success() with this string on copy */
successMessage?: string;
/** Called after successful copy */
onSuccess?: () => void;
/** Called on copy failure (clipboard error or text getter error) */
onError?: (error: unknown) => void;
/** Additional inline style */
style?: React.CSSProperties;
/** Additional className */
className?: string;
}
export const CopyButton = React.forwardRef<HTMLElement, CopyButtonProps>(
function CopyButton(
{ text, size = 14, timeout = 2000, successMessage, onSuccess, onError, style, className },
ref,
) {
const { token } = theme.useToken();
const { copy, isCopied } = useCopyToClipboard({ timeout });
const handleClick = useCallback(async () => {
try {
const value = typeof text === 'function' ? await text() : text;
const ok = await copy(value);
if (ok) {
if (successMessage) message.success(successMessage);
onSuccess?.();
}
} catch (e) {
onError?.(e);
}
}, [text, copy, successMessage, onSuccess, onError]);
return (
<Button
ref={ref as React.Ref<HTMLButtonElement>}
type="text"
size="small"
icon={isCopied ? <Check size={size} style={{ color: token.colorSuccess }} /> : <Copy size={size} />}
onClick={handleClick}
style={style}
className={className}
/>
);
},
);
@@ -0,0 +1,237 @@
import { Slider, InputNumber, Switch, Tooltip, Divider, theme } from 'antd';
import { Info } from 'lucide-react';
import { useTranslation } from 'react-i18next';
// ── Single parameter row with optional switch ──────────────
interface ParamRowProps {
label: string;
tooltip?: string;
value: number | null;
defaultValue: number;
min: number;
max: number;
step: number;
marks?: Record<string | number, string>;
inputWidth?: number;
onChange: (v: number | null) => void;
showSwitch?: boolean;
showDivider?: boolean;
}
function ParamRow({
label,
tooltip,
value,
defaultValue,
min,
max,
step,
marks,
inputWidth = 70,
onChange,
showSwitch = true,
showDivider = true,
}: ParamRowProps) {
const { token } = theme.useToken();
const isOn = value !== null;
return (
<>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 0' }}>
<span style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 13 }}>
{label}
{tooltip && (
<Tooltip title={tooltip}>
<Info size={12} style={{ color: token.colorTextSecondary, cursor: 'help' }} />
</Tooltip>
)}
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{isOn && (
<InputNumber
style={{ width: inputWidth }}
min={min}
max={max}
step={step}
value={value!}
onChange={(v) => v !== null && onChange(v)}
size="small"
/>
)}
{showSwitch && (
<Switch
size="small"
checked={isOn}
onChange={(checked) => onChange(checked ? defaultValue : null)}
/>
)}
</span>
</div>
{isOn && (
<div style={{ paddingBottom: 8 }}>
<Slider
min={min}
max={max}
step={step}
marks={marks}
value={value!}
onChange={(v) => onChange(v)}
/>
</div>
)}
{showDivider && <Divider style={{ margin: 0 }} />}
</>
);
}
// ── Exported composite component ───────────────────────────
export interface ModelParamValues {
temperature: number | null;
topP: number | null;
maxTokens: number | null;
frequencyPenalty: number | null;
}
export interface ModelParamDefaults {
temperature?: number;
topP?: number;
maxTokens?: number;
frequencyPenalty?: number;
}
export interface ModelParamSlidersProps {
values: ModelParamValues;
onChange: (values: Partial<ModelParamValues>) => void;
/** Default values when toggling a param ON (defaults: temp=0.7, topP=1.0, maxTokens=4096, freqPenalty=0) */
defaults?: ModelParamDefaults;
/** Show parameter toggle switches (default: true) */
showSwitch?: boolean;
/** Show dividers between rows (default: true) */
showDividers?: boolean;
/** Which parameters to render (default: all four) */
visibleParams?: Array<'temperature' | 'topP' | 'maxTokens' | 'frequencyPenalty'>;
}
const MAX_TOKENS_MARKS: Record<string | number, string> = {
256: '', 32768: '32K', 131072: '128K', 1048576: '1M',
};
const TEMPERATURE_MARKS: Record<string | number, string> = {
0: '0', 0.5: '', 1: '1', 1.5: '', 2: '2',
};
const TOP_P_MARKS: Record<string | number, string> = {
0: '0', 0.5: '', 1: '1',
};
const FREQ_PENALTY_MARKS: Record<string | number, string> = {
'-2': '-2', 0: '0', 2: '2',
};
const DEFAULT_DEFAULTS: Required<ModelParamDefaults> = {
temperature: 0.7,
topP: 1.0,
maxTokens: 4096,
frequencyPenalty: 0,
};
const ALL_PARAMS = ['temperature', 'topP', 'maxTokens', 'frequencyPenalty'] as const;
export function ModelParamSliders({
values,
onChange,
defaults,
showSwitch = true,
showDividers = true,
visibleParams = [...ALL_PARAMS],
}: ModelParamSlidersProps) {
const { t } = useTranslation();
const d = { ...DEFAULT_DEFAULTS, ...defaults };
const visible = new Set(visibleParams);
const visibleList = ALL_PARAMS.filter((p) => visible.has(p));
return (
<>
{visibleList.map((param, idx) => {
const isLast = idx === visibleList.length - 1;
switch (param) {
case 'temperature':
return (
<ParamRow
key="temperature"
label={t('settings.temperature')}
tooltip={t('settings.temperatureTooltip')}
value={values.temperature}
defaultValue={d.temperature}
min={0}
max={2}
step={0.1}
marks={TEMPERATURE_MARKS}
onChange={(v) => onChange({ temperature: v })}
showSwitch={showSwitch}
showDivider={showDividers && !isLast}
/>
);
case 'topP':
return (
<ParamRow
key="topP"
label="Top P"
tooltip={t('settings.topPTooltip')}
value={values.topP}
defaultValue={d.topP}
min={0}
max={1}
step={0.05}
marks={TOP_P_MARKS}
onChange={(v) => onChange({ topP: v })}
showSwitch={showSwitch}
showDivider={showDividers && !isLast}
/>
);
case 'maxTokens':
return (
<ParamRow
key="maxTokens"
label={t('settings.maxTokens')}
tooltip={t('settings.maxTokensTooltip')}
value={values.maxTokens}
defaultValue={d.maxTokens}
min={256}
max={1048576}
step={256}
marks={MAX_TOKENS_MARKS}
inputWidth={90}
onChange={(v) => onChange({ maxTokens: v })}
showSwitch={showSwitch}
showDivider={showDividers && !isLast}
/>
);
case 'frequencyPenalty':
return (
<ParamRow
key="frequencyPenalty"
label={t('settings.frequencyPenalty')}
tooltip={t('settings.frequencyPenaltyTooltip')}
value={values.frequencyPenalty}
defaultValue={d.frequencyPenalty}
min={-2}
max={2}
step={0.1}
marks={FREQ_PENALTY_MARKS}
onChange={(v) => onChange({ frequencyPenalty: v })}
showSwitch={showSwitch}
showDivider={showDividers && !isLast}
/>
);
default:
return null;
}
})}
</>
);
}