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:
@@ -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;
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user