refactor(app): rename the desktop app and refine the interface
This commit is contained in:
1 parent
91032f3a06
commit
ad02c2a8a6
15 files changed
+206
-87
No files matched your search
+39
-4
@@ -1,5 +1,18 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Bot, FileKey, Gauge, GitFork, History, LogIn, Network, ServerCog, Settings } from 'lucide-react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import {
|
||||
Bot,
|
||||
ExternalLink,
|
||||
FileKey,
|
||||
Gauge,
|
||||
GitFork,
|
||||
History,
|
||||
LogIn,
|
||||
MessageCircle,
|
||||
Network,
|
||||
ServerCog,
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
import appLogo from './assets/logo.jpg';
|
||||
import { CoreRuntimeProvider, useCoreRuntime } from './coreRuntime';
|
||||
import { ConfigPanelPage } from './pages/ConfigPanel';
|
||||
@@ -12,6 +25,8 @@ import { AgentsPage } from './pages/AgentsPage';
|
||||
import { ThinkingAliasesPage } from './pages/ThinkingAliasesPage';
|
||||
import { UsageRecordsPage } from './pages/UsageRecordsPage';
|
||||
|
||||
const CONTACT_URL = 'https://qm.qq.com/q/3queDaIG';
|
||||
|
||||
const pages = [
|
||||
{
|
||||
id: 'kernel',
|
||||
@@ -99,6 +114,14 @@ function AppContent() {
|
||||
setActive(pageId);
|
||||
};
|
||||
|
||||
const openContact = async () => {
|
||||
try {
|
||||
await invoke('open_external_url', { url: CONTACT_URL });
|
||||
} catch (error) {
|
||||
console.error('打开联系我们链接失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<aside className="sidebar">
|
||||
@@ -132,9 +155,21 @@ function AppContent() {
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<span>CPA GUI</span>
|
||||
<strong>0.1</strong>
|
||||
<div className="sidebar-bottom">
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar-contact"
|
||||
title="通过 QQ 联系我们"
|
||||
onClick={() => void openContact()}
|
||||
>
|
||||
<MessageCircle size={16} aria-hidden="true" />
|
||||
<span>联系我们</span>
|
||||
<ExternalLink size={13} aria-hidden="true" />
|
||||
</button>
|
||||
<div className="sidebar-footer">
|
||||
<span>Easy_CLIProxyAPI</span>
|
||||
<strong>0.1</strong>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export class AppErrorBoundary extends Component<Props, State> {
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||
console.error('CPA GUI 渲染异常', error, info.componentStack);
|
||||
console.error('Easy_CLIProxyAPI 渲染异常', error, info.componentStack);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@@ -215,6 +216,13 @@ type AgentModelPickerProps = {
|
||||
onRefresh: () => void;
|
||||
};
|
||||
|
||||
type AgentModelDropdownLayout = {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
function AgentModelPicker({
|
||||
models,
|
||||
value,
|
||||
@@ -227,6 +235,7 @@ function AgentModelPicker({
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [dropdownLayout, setDropdownLayout] = useState<AgentModelDropdownLayout | null>(null);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
const visibleModels = useMemo(() => filterAgentModels(models, search), [models, search]);
|
||||
@@ -236,6 +245,51 @@ function AgentModelPicker({
|
||||
);
|
||||
const selectedAlias = agentModelAlias(models, value);
|
||||
|
||||
const updateDropdownLayout = useCallback(() => {
|
||||
const root = rootRef.current;
|
||||
if (!root) return;
|
||||
|
||||
const rect = root.getBoundingClientRect();
|
||||
const edgeGap = 12;
|
||||
const triggerGap = 6;
|
||||
const preferredHeight = 282;
|
||||
const minimumHeight = 150;
|
||||
const spaceBelow = Math.max(0, window.innerHeight - rect.bottom - triggerGap - edgeGap);
|
||||
const spaceAbove = Math.max(0, rect.top - triggerGap - edgeGap);
|
||||
const placeAbove = spaceBelow < preferredHeight && spaceAbove > spaceBelow;
|
||||
const availableHeight = placeAbove ? spaceAbove : spaceBelow;
|
||||
const height = Math.min(preferredHeight, Math.max(minimumHeight, availableHeight));
|
||||
const width = Math.min(rect.width, window.innerWidth - edgeGap * 2);
|
||||
const left = Math.min(
|
||||
Math.max(edgeGap, rect.left),
|
||||
Math.max(edgeGap, window.innerWidth - edgeGap - width),
|
||||
);
|
||||
const desiredTop = placeAbove
|
||||
? rect.top - triggerGap - height
|
||||
: rect.bottom + triggerGap;
|
||||
const top = Math.min(
|
||||
Math.max(edgeGap, desiredTop),
|
||||
Math.max(edgeGap, window.innerHeight - edgeGap - height),
|
||||
);
|
||||
|
||||
setDropdownLayout({ top, left, width, height });
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open) {
|
||||
setDropdownLayout(null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
updateDropdownLayout();
|
||||
window.addEventListener('resize', updateDropdownLayout);
|
||||
window.addEventListener('scroll', updateDropdownLayout);
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateDropdownLayout);
|
||||
window.removeEventListener('scroll', updateDropdownLayout);
|
||||
};
|
||||
}, [open, updateDropdownLayout]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
const close = (event: MouseEvent) => {
|
||||
@@ -311,7 +365,12 @@ function AgentModelPicker({
|
||||
</button>
|
||||
|
||||
{open ? (
|
||||
<div className="agent-model-dropdown">
|
||||
<div
|
||||
className="agent-model-dropdown"
|
||||
style={dropdownLayout
|
||||
? dropdownLayout
|
||||
: { top: 0, left: 0, width: 0, height: 0, visibility: 'hidden' }}
|
||||
>
|
||||
<div className="agent-model-search">
|
||||
<Search size={15} aria-hidden />
|
||||
<input
|
||||
|
||||
@@ -82,17 +82,6 @@ function requestLatestCore() {
|
||||
return latestCheckPromise;
|
||||
}
|
||||
|
||||
const compareVersions = (left: string, right: string) => {
|
||||
const parse = (value: string) => value.replace(/^v/i, '').split('.').map((part) => Number(part) || 0);
|
||||
const leftParts = parse(left);
|
||||
const rightParts = parse(right);
|
||||
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
||||
const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
||||
if (difference !== 0) return difference;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export function KernelPage() {
|
||||
const {
|
||||
status: coreStatus,
|
||||
@@ -491,13 +480,6 @@ export function KernelPage() {
|
||||
|
||||
const installBundledCore = async () => {
|
||||
if (!bundledCore) return;
|
||||
const comparison = currentVersion ? compareVersions(currentVersion, bundledCore.version) : 0;
|
||||
const prompt = comparison > 0
|
||||
? `当前内核 ${currentVersion} 高于内置版本 ${bundledCore.version},继续会降级。是否确认?`
|
||||
: currentVersion === bundledCore.version
|
||||
? `将使用内置压缩包重新安装 ${bundledCore.version},是否继续?`
|
||||
: `将安装内置内核 ${bundledCore.version},是否继续?`;
|
||||
if (!window.confirm(prompt)) return;
|
||||
|
||||
setInstalling(true);
|
||||
setMessage(`正在安装内置内核 ${bundledCore.version}`);
|
||||
|
||||
+49
-13
@@ -181,14 +181,52 @@ input:focus-visible {
|
||||
color: #2d2a26;
|
||||
}
|
||||
|
||||
.sidebar-bottom {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: auto;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid #e3e1db;
|
||||
}
|
||||
|
||||
.sidebar-contact {
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(0, 1fr) 14px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #d8d4cc;
|
||||
border-radius: 7px;
|
||||
background: #fffdf8;
|
||||
color: #4f4a43;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sidebar-contact:hover {
|
||||
border-color: #c7c2b8;
|
||||
background: #e8e5dc;
|
||||
color: #2d2a26;
|
||||
}
|
||||
|
||||
.sidebar-contact svg {
|
||||
color: #7a746c;
|
||||
}
|
||||
|
||||
.sidebar-contact span {
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: auto;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid #e3e1db;
|
||||
color: #7a746c;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -3600,13 +3638,10 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.agent-model-dropdown {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
z-index: 45;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
grid-template-rows: 40px 260px 28px;
|
||||
grid-template-rows: 40px minmax(0, 1fr) 28px;
|
||||
gap: 7px;
|
||||
padding: 9px;
|
||||
border: 1px solid #e3e1db;
|
||||
@@ -4880,6 +4915,12 @@ input:focus-visible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-bottom {
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 18px;
|
||||
}
|
||||
@@ -5132,11 +5173,6 @@ input:focus-visible {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.agent-model-dropdown {
|
||||
right: -4px;
|
||||
left: -4px;
|
||||
}
|
||||
|
||||
.agent-config-footer .primary-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user