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
@@ -112,23 +112,37 @@ jobs:
|
||||
run: >-
|
||||
node scripts/prepare-portable.mjs
|
||||
--binary "src-tauri/target/release/cpa-gui${{ matrix.os == 'windows' && '.exe' || '' }}"
|
||||
--output "release/cpa-gui-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
--output "release/Easy_CLIProxyAPI-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
--os "${{ matrix.os }}"
|
||||
--arch "${{ matrix.arch }}"
|
||||
--download true
|
||||
--clean-core true
|
||||
shell: bash
|
||||
- name: Verify archive-only core payload
|
||||
shell: bash
|
||||
env:
|
||||
CORE_DIR: release/Easy_CLIProxyAPI-${{ matrix.os }}-${{ matrix.arch }}/cpa-core
|
||||
run: |
|
||||
node - <<'NODE'
|
||||
const { readdirSync } = require('node:fs');
|
||||
const entries = readdirSync(process.env.CORE_DIR, { withFileTypes: true });
|
||||
const valid = entries.length === 1
|
||||
&& entries[0].isFile()
|
||||
&& /\.(zip|tar\.gz)$/.test(entries[0].name);
|
||||
if (!valid) {
|
||||
throw new Error(`cpa-core must contain exactly one archive; found: ${entries.map((entry) => entry.name).join(', ')}`);
|
||||
}
|
||||
NODE
|
||||
- name: Archive Unix release
|
||||
if: matrix.os != 'windows'
|
||||
run: tar -czf "cpa-gui-${{ matrix.os }}-${{ matrix.arch }}.tar.gz" -C release "cpa-gui-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
run: tar -czf "Easy_CLIProxyAPI-${{ matrix.os }}-${{ matrix.arch }}.tar.gz" -C release "Easy_CLIProxyAPI-${{ matrix.os }}-${{ matrix.arch }}"
|
||||
- name: Archive Windows release
|
||||
if: matrix.os == 'windows'
|
||||
shell: pwsh
|
||||
run: Compress-Archive -Path "release/cpa-gui-${{ matrix.os }}-${{ matrix.arch }}" -DestinationPath "cpa-gui-${{ matrix.os }}-${{ matrix.arch }}.zip"
|
||||
run: Compress-Archive -Path "release/Easy_CLIProxyAPI-${{ matrix.os }}-${{ matrix.arch }}" -DestinationPath "Easy_CLIProxyAPI-${{ matrix.os }}-${{ matrix.arch }}.zip"
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cpa-gui-${{ matrix.os }}-${{ matrix.arch }}
|
||||
path: cpa-gui-${{ matrix.os }}-${{ matrix.arch }}.${{ matrix.suffix }}
|
||||
name: Easy_CLIProxyAPI-${{ matrix.os }}-${{ matrix.arch }}
|
||||
path: Easy_CLIProxyAPI-${{ matrix.os }}-${{ matrix.arch }}.${{ matrix.suffix }}
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
@@ -159,7 +173,7 @@ jobs:
|
||||
fi
|
||||
|
||||
prerelease="$MANUAL_PRERELEASE"
|
||||
release_args=(--title "CPA GUI $RELEASE_TAG" --generate-notes)
|
||||
release_args=(--title "Easy_CLIProxyAPI $RELEASE_TAG" --generate-notes)
|
||||
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
|
||||
release_args+=(--verify-tag)
|
||||
if [[ "$RELEASE_TAG" == *-* ]]; then
|
||||
|
||||
@@ -9,7 +9,7 @@ $ErrorActionPreference = 'Stop'
|
||||
$RootDir = $PSScriptRoot
|
||||
$AppBin = Join-Path $RootDir 'src-tauri\target\release\cpa-gui.exe'
|
||||
$BinDir = Join-Path $RootDir 'bin-work'
|
||||
$BinOut = Join-Path $BinDir 'cpa-gui.exe'
|
||||
$BinOut = Join-Path $BinDir 'Easy_CLIProxyAPI.exe'
|
||||
$PreparePortable = Join-Path $RootDir 'scripts\prepare-portable.mjs'
|
||||
|
||||
Set-Location -LiteralPath $RootDir
|
||||
@@ -67,5 +67,16 @@ if (-not (Test-Path -LiteralPath $BinOut -PathType Leaf)) {
|
||||
throw "Portable preparation finished, but executable not found: $BinOut"
|
||||
}
|
||||
|
||||
$CoreOut = Join-Path $BinDir 'cpa-core'
|
||||
$CoreEntries = @(Get-ChildItem -LiteralPath $CoreOut -Force)
|
||||
if (
|
||||
$CoreEntries.Count -ne 1 -or
|
||||
$CoreEntries[0].PSIsContainer -or
|
||||
$CoreEntries[0].Name -notmatch '^CLIProxyAPI_.+_windows_.+\.zip$'
|
||||
) {
|
||||
throw "Portable core output must contain exactly one Windows core archive: $CoreOut"
|
||||
}
|
||||
|
||||
Write-Host "Built: $AppBin"
|
||||
Write-Host "Copied: $BinOut"
|
||||
Write-Host "Bundled core archive: $($CoreEntries[0].FullName)"
|
||||
@@ -7,7 +7,7 @@ cd "$ROOT_DIR"
|
||||
BUILD_JOBS="${CARGO_BUILD_JOBS:-20}"
|
||||
APP_BIN="$ROOT_DIR/src-tauri/target/release/cpa-gui"
|
||||
BIN_DIR="$ROOT_DIR/bin-work"
|
||||
BIN_OUT="$BIN_DIR/cpa-gui"
|
||||
BIN_OUT="$BIN_DIR/Easy_CLIProxyAPI"
|
||||
|
||||
if ! command -v bun >/dev/null 2>&1; then
|
||||
echo "bun is not installed or not in PATH."
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CPA GUI</title>
|
||||
<title>Easy_CLIProxyAPI</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -6,7 +6,7 @@ $ErrorActionPreference = 'Stop'
|
||||
|
||||
$RootDir = $PSScriptRoot
|
||||
$BinDir = Join-Path $RootDir 'bin-work'
|
||||
$AppBin = Join-Path $BinDir 'cpa-gui.exe'
|
||||
$AppBin = Join-Path $BinDir 'Easy_CLIProxyAPI.exe'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $AppBin -PathType Leaf)) {
|
||||
Write-Host "Executable not found: $AppBin"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
APP_BIN="$ROOT_DIR/bin-work/cpa-gui"
|
||||
APP_BIN="$ROOT_DIR/bin-work/Easy_CLIProxyAPI"
|
||||
|
||||
if [ ! -x "$APP_BIN" ]; then
|
||||
echo "Executable not found: $APP_BIN"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { chmod, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { chmod, copyFile, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { basename, join, resolve } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const args = new Map();
|
||||
for (let index = 2; index < process.argv.length; index += 2) {
|
||||
@@ -15,7 +14,6 @@ const binary = resolve(args.get('--binary') ?? join(root, 'src-tauri', 'target',
|
||||
const targetOS = args.get('--os') ?? ({ linux: 'linux', darwin: 'darwin', win32: 'windows' })[process.platform];
|
||||
const targetArch = args.get('--arch') ?? ({ x64: 'amd64', arm64: 'aarch64' })[process.arch];
|
||||
const shouldDownload = args.get('--download') === 'true';
|
||||
const cleanCore = args.get('--clean-core') === 'true';
|
||||
|
||||
if (!targetOS || !targetArch) throw new Error(`Unsupported target: ${process.platform}/${process.arch}`);
|
||||
if (!existsSync(binary)) throw new Error(`GUI binary not found: ${binary}`);
|
||||
@@ -32,13 +30,7 @@ const checksumsPath = join(sourceDir, 'checksums.txt');
|
||||
await mkdir(sourceDir, { recursive: true });
|
||||
if (!existsSync(sourceArchive)) {
|
||||
if (!shouldDownload) {
|
||||
console.warn(`Built-in core archive not found; portable output keeps the existing core: ${sourceArchive}`);
|
||||
await mkdir(output, { recursive: true });
|
||||
const outputBinary = join(output, targetOS === 'windows' ? 'cpa-gui.exe' : 'cpa-gui');
|
||||
await copyFile(binary, outputBinary);
|
||||
if (targetOS !== 'windows') await chmod(outputBinary, 0o755);
|
||||
await copyFile(join(root, 'core-version.txt'), join(output, 'core-version.txt'));
|
||||
process.exit(0);
|
||||
throw new Error(`Built-in core archive not found: ${sourceArchive}`);
|
||||
}
|
||||
const tag = `v${version}`;
|
||||
const releaseBase = `https://github.com/router-for-me/CLIProxyAPI/releases/download/${tag}`;
|
||||
@@ -61,35 +53,25 @@ if (existsSync(checksumsPath)) {
|
||||
}
|
||||
|
||||
await mkdir(output, { recursive: true });
|
||||
const outputBinary = join(output, targetOS === 'windows' ? 'cpa-gui.exe' : 'cpa-gui');
|
||||
const outputBinary = join(output, targetOS === 'windows' ? 'Easy_CLIProxyAPI.exe' : 'Easy_CLIProxyAPI');
|
||||
const legacyOutputBinary = join(output, targetOS === 'windows' ? 'cpa-gui.exe' : 'cpa-gui');
|
||||
await rm(legacyOutputBinary, { force: true });
|
||||
await copyFile(binary, outputBinary);
|
||||
if (targetOS !== 'windows') await chmod(outputBinary, 0o755);
|
||||
await copyFile(join(root, 'core-version.txt'), join(output, 'core-version.txt'));
|
||||
|
||||
const coreOutput = join(output, 'cpa-core');
|
||||
if (cleanCore) await rm(coreOutput, { recursive: true, force: true });
|
||||
await rm(coreOutput, { recursive: true, force: true });
|
||||
await mkdir(coreOutput, { recursive: true });
|
||||
let extraction;
|
||||
if (extension === 'zip') {
|
||||
if (process.platform === 'win32') {
|
||||
const quotePowerShell = (value) => value.replaceAll("'", "''");
|
||||
const command = `Expand-Archive -LiteralPath '${quotePowerShell(sourceArchive)}' -DestinationPath '${quotePowerShell(coreOutput)}' -Force`;
|
||||
extraction = spawnSync('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command], { stdio: 'inherit' });
|
||||
} else {
|
||||
extraction = spawnSync('unzip', ['-q', sourceArchive, '-d', coreOutput], { stdio: 'inherit' });
|
||||
}
|
||||
} else {
|
||||
extraction = spawnSync('tar', ['-xf', sourceArchive, '-C', coreOutput], { stdio: 'inherit' });
|
||||
}
|
||||
if (extraction.error) throw new Error(`Failed to start archive extractor for ${assetName}: ${extraction.error.message}`);
|
||||
if (extraction.status !== 0) throw new Error(`Failed to extract ${assetName} (exit code ${extraction.status})`);
|
||||
await copyFile(sourceArchive, join(coreOutput, assetName));
|
||||
if (existsSync(checksumsPath)) await copyFile(checksumsPath, join(coreOutput, 'checksums.txt'));
|
||||
await writeFile(join(coreOutput, 'cpa-gui-meta.json'), `${JSON.stringify({
|
||||
version: `v${version}`,
|
||||
assetName,
|
||||
installedAtUnix: Math.floor(Date.now() / 1000),
|
||||
}, null, 2)}\n`);
|
||||
const coreEntries = await readdir(coreOutput, { withFileTypes: true });
|
||||
if (
|
||||
coreEntries.length !== 1
|
||||
|| !coreEntries[0].isFile()
|
||||
|| coreEntries[0].name !== assetName
|
||||
) {
|
||||
throw new Error(`Core output must contain only ${assetName}`);
|
||||
}
|
||||
|
||||
console.log(`Prepared portable directory: ${output}`);
|
||||
console.log(`Bundled core: ${basename(sourceArchive)}`);
|
||||
console.log(`Bundled core: ${basename(sourceArchive)} (archive only)`);
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default permissions for the main CPA GUI window.",
|
||||
"description": "Default permissions for the main Easy_CLIProxyAPI window.",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:event:allow-listen", "core:event:allow-unlisten"]
|
||||
}
|
||||
@@ -808,7 +808,7 @@ struct GithubAsset {
|
||||
|
||||
#[tauri::command]
|
||||
fn health_check() -> &'static str {
|
||||
"CPA GUI Rust backend is ready"
|
||||
"Easy_CLIProxyAPI Rust backend is ready"
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -5382,7 +5382,7 @@ fn configure_child_lifetime(command: &mut Command) {
|
||||
|
||||
if libc::getppid() != parent_process_id {
|
||||
return Err(io::Error::other(
|
||||
"CPA GUI exited before the core process started",
|
||||
"Easy_CLIProxyAPI exited before the core process started",
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CPA GUI",
|
||||
"productName": "Easy_CLIProxyAPI",
|
||||
"version": "0.1.0-pre.4",
|
||||
"identifier": "com.cpa.gui",
|
||||
"build": {
|
||||
@@ -13,7 +13,7 @@
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "CPA GUI",
|
||||
"title": "Easy_CLIProxyAPI",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"minWidth": 900,
|
||||
|
||||
+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