mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
release: v0.55.0 - 代码重构与 ESLint 修复
- 更新版本号到 0.55.0 - 修复 ESLint 警告:将 CREATION_MODE_CONFIG 移至单独的 constants.tsx - 修复 ESLint 警告:将 CreationMode 类型移至 types.ts - 修复 ProviderPoolPage 未使用变量警告 - 修复 Rust 测试中缺少的 Config 字段 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "proxycast",
|
||||
"private": true,
|
||||
"version": "0.54.0",
|
||||
"version": "0.55.0",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Generated
+3
-3
@@ -6553,7 +6553,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proxycast"
|
||||
version = "0.54.0"
|
||||
version = "0.55.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
@@ -6635,7 +6635,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proxycast-core"
|
||||
version = "0.54.0"
|
||||
version = "0.55.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
@@ -6651,7 +6651,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proxycast-infra"
|
||||
version = "0.54.0"
|
||||
version = "0.55.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dashmap 5.5.3",
|
||||
|
||||
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.54.0"
|
||||
version = "0.55.0"
|
||||
edition = "2021"
|
||||
authors = ["you"]
|
||||
repository = "https://github.com/aiclientproxy/proxycast"
|
||||
@@ -164,7 +164,7 @@ version = "2.4"
|
||||
|
||||
[package]
|
||||
name = "proxycast"
|
||||
version = "0.54.0"
|
||||
version = "0.55.0"
|
||||
description = "AI API Proxy Desktop App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! 使用 proptest 进行属性测试
|
||||
|
||||
use crate::config::types::{ContentCreatorConfig, NavigationConfig};
|
||||
use crate::config::{
|
||||
collapse_tilde, contains_tilde, expand_tilde, Config, ConfigManager, CustomProviderConfig,
|
||||
HotReloadManager, InjectionSettings, LoggingConfig, ProviderConfig, ProvidersConfig,
|
||||
@@ -189,6 +190,8 @@ fn arb_config() -> impl Strategy<Value = Config> {
|
||||
agent: crate::config::NativeAgentConfig::default(),
|
||||
language: "zh".to_string(),
|
||||
experimental: crate::config::ExperimentalFeatures::default(),
|
||||
content_creator: ContentCreatorConfig::default(),
|
||||
navigation: NavigationConfig::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -427,6 +430,8 @@ fn arb_valid_config() -> impl Strategy<Value = Config> {
|
||||
agent: crate::config::NativeAgentConfig::default(),
|
||||
language: "zh".to_string(),
|
||||
experimental: crate::config::ExperimentalFeatures::default(),
|
||||
content_creator: ContentCreatorConfig::default(),
|
||||
navigation: NavigationConfig::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -476,6 +481,8 @@ fn arb_invalid_config() -> impl Strategy<Value = Config> {
|
||||
agent: crate::config::NativeAgentConfig::default(),
|
||||
language: "zh".to_string(),
|
||||
experimental: crate::config::ExperimentalFeatures::default(),
|
||||
content_creator: ContentCreatorConfig::default(),
|
||||
navigation: NavigationConfig::default(),
|
||||
};
|
||||
// 根据类型使配置无效
|
||||
match invalid_type {
|
||||
|
||||
@@ -425,6 +425,12 @@ pub struct Config {
|
||||
/// 实验室功能配置
|
||||
#[serde(default)]
|
||||
pub experimental: ExperimentalFeatures,
|
||||
/// 内容创作配置
|
||||
#[serde(default)]
|
||||
pub content_creator: ContentCreatorConfig,
|
||||
/// 导航栏配置
|
||||
#[serde(default)]
|
||||
pub navigation: NavigationConfig,
|
||||
}
|
||||
|
||||
// ============ Native Agent 配置类型 ============
|
||||
@@ -487,6 +493,67 @@ impl Default for NativeAgentConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 内容创作配置类型 ============
|
||||
|
||||
/// 内容创作主题配置
|
||||
///
|
||||
/// 配置内容创作模式中显示的主题标签
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ContentCreatorConfig {
|
||||
/// 启用的主题列表
|
||||
#[serde(default = "default_enabled_themes")]
|
||||
pub enabled_themes: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_enabled_themes() -> Vec<String> {
|
||||
vec![
|
||||
"general".to_string(),
|
||||
"social-media".to_string(),
|
||||
"poster".to_string(),
|
||||
"music".to_string(),
|
||||
"video".to_string(),
|
||||
"novel".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
impl Default for ContentCreatorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled_themes: default_enabled_themes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 导航栏配置类型 ============
|
||||
|
||||
/// 导航栏模块配置
|
||||
///
|
||||
/// 配置左侧导航栏中显示的功能模块
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct NavigationConfig {
|
||||
/// 启用的导航模块列表
|
||||
#[serde(default = "default_enabled_nav_items")]
|
||||
pub enabled_items: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_enabled_nav_items() -> Vec<String> {
|
||||
vec![
|
||||
"agent".to_string(),
|
||||
"projects".to_string(),
|
||||
"image-gen".to_string(),
|
||||
"api-server".to_string(),
|
||||
"provider-pool".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
impl Default for NavigationConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled_items: default_enabled_nav_items(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 实验室功能配置类型 ============
|
||||
|
||||
/// 截图对话功能配置
|
||||
@@ -1653,6 +1720,8 @@ impl Default for Config {
|
||||
models: ModelsConfig::default(),
|
||||
agent: NativeAgentConfig::default(),
|
||||
experimental: ExperimentalFeatures::default(),
|
||||
content_creator: ContentCreatorConfig::default(),
|
||||
navigation: NavigationConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 类似 cherry-studio 的图标导航栏,始终显示在应用左侧
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import { getPluginsForSurface, PluginUIInfo } from "@/lib/api/pluginUI";
|
||||
import { Page, PageParams } from "@/types/page";
|
||||
import { getConfig } from "@/hooks/useTauri";
|
||||
|
||||
interface AppSidebarProps {
|
||||
currentPage: Page;
|
||||
@@ -143,6 +144,15 @@ function getIconByName(iconName: string): LucideIcon {
|
||||
return IconComponent || Activity;
|
||||
}
|
||||
|
||||
/** 默认启用的导航模块 */
|
||||
const DEFAULT_ENABLED_NAV_ITEMS = [
|
||||
"agent",
|
||||
"projects",
|
||||
"image-gen",
|
||||
"api-server",
|
||||
"provider-pool",
|
||||
];
|
||||
|
||||
export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) {
|
||||
const [theme, setTheme] = useState<"light" | "dark">(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -153,11 +163,46 @@ export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) {
|
||||
return "light";
|
||||
});
|
||||
|
||||
// 启用的导航模块
|
||||
const [enabledNavItems, setEnabledNavItems] = useState<string[]>(
|
||||
DEFAULT_ENABLED_NAV_ITEMS,
|
||||
);
|
||||
|
||||
// 已安装的侧边栏插件列表
|
||||
const [sidebarPlugins, setSidebarPlugins] = useState<PluginUIInfo[]>([]);
|
||||
// 刷新触发器
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
||||
|
||||
// 加载导航配置
|
||||
useEffect(() => {
|
||||
const loadNavConfig = async () => {
|
||||
try {
|
||||
const config = await getConfig();
|
||||
setEnabledNavItems(
|
||||
config.navigation?.enabled_items || DEFAULT_ENABLED_NAV_ITEMS,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("加载导航配置失败:", error);
|
||||
}
|
||||
};
|
||||
loadNavConfig();
|
||||
|
||||
// 监听导航配置变更事件
|
||||
const handleNavConfigChange = () => {
|
||||
loadNavConfig();
|
||||
};
|
||||
window.addEventListener("nav-config-changed", handleNavConfigChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("nav-config-changed", handleNavConfigChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 过滤后的导航项
|
||||
const filteredMenuItems = useMemo(() => {
|
||||
return mainMenuItems.filter((item) => enabledNavItems.includes(item.id));
|
||||
}, [enabledNavItems]);
|
||||
|
||||
// 加载侧边栏插件
|
||||
useEffect(() => {
|
||||
const loadSidebarPlugins = async () => {
|
||||
@@ -221,7 +266,7 @@ export function AppSidebar({ currentPage, onNavigate }: AppSidebarProps) {
|
||||
</Tooltip>
|
||||
|
||||
<MenusContainer>
|
||||
{mainMenuItems.map((item) => (
|
||||
{filteredMenuItems.map((item) => (
|
||||
<Tooltip key={item.id}>
|
||||
<TooltipTrigger asChild>
|
||||
<IconButton
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import styled, { keyframes, css } from "styled-components";
|
||||
import {
|
||||
ArrowRight,
|
||||
@@ -12,61 +11,11 @@ import {
|
||||
ChevronDown,
|
||||
Search,
|
||||
Globe,
|
||||
GraduationCap,
|
||||
Zap,
|
||||
RefreshCw,
|
||||
LayoutTemplate,
|
||||
Music,
|
||||
} from "lucide-react";
|
||||
|
||||
/**
|
||||
* 创作模式类型
|
||||
* 不同模式下 AI 的角色和用户参与度不同
|
||||
*/
|
||||
export type CreationMode = "guided" | "fast" | "hybrid" | "framework";
|
||||
|
||||
/**
|
||||
* 模式配置
|
||||
*/
|
||||
export const CREATION_MODE_CONFIG: Record<
|
||||
CreationMode,
|
||||
{
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
aiRole: string;
|
||||
userInvolvement: "high" | "medium" | "low";
|
||||
description: string;
|
||||
}
|
||||
> = {
|
||||
guided: {
|
||||
name: "引导模式",
|
||||
icon: <GraduationCap className="w-4 h-4" />,
|
||||
aiRole: "教练(提问引导)",
|
||||
userInvolvement: "high",
|
||||
description: "追求真实性、个人经历类内容",
|
||||
},
|
||||
fast: {
|
||||
name: "快速模式",
|
||||
icon: <Zap className="w-4 h-4" />,
|
||||
aiRole: "助手(生成初稿)",
|
||||
userInvolvement: "low",
|
||||
description: "信息整理、快速产出",
|
||||
},
|
||||
hybrid: {
|
||||
name: "混合模式",
|
||||
icon: <RefreshCw className="w-4 h-4" />,
|
||||
aiRole: "协作者(写框架)",
|
||||
userInvolvement: "medium",
|
||||
description: "平衡质量和效率",
|
||||
},
|
||||
framework: {
|
||||
name: "框架模式",
|
||||
icon: <LayoutTemplate className="w-4 h-4" />,
|
||||
aiRole: "填充者(按框架生成)",
|
||||
userInvolvement: "medium",
|
||||
description: "固定格式文档(报告、标书)",
|
||||
},
|
||||
};
|
||||
import { getConfig } from "@/hooks/useTauri";
|
||||
import type { CreationMode } from "./types";
|
||||
import { CREATION_MODE_CONFIG } from "./constants";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
@@ -351,7 +300,7 @@ interface EmptyStateProps {
|
||||
}
|
||||
|
||||
// Scenarios Configuration - 与 ProjectType 统一
|
||||
const CATEGORIES = [
|
||||
const ALL_CATEGORIES = [
|
||||
{
|
||||
id: "general",
|
||||
label: "通用对话",
|
||||
@@ -379,6 +328,16 @@ const CATEGORIES = [
|
||||
{ id: "novel", label: "小说创作", icon: <PenTool className="w-4 h-4" /> },
|
||||
];
|
||||
|
||||
/** 默认启用的主题 */
|
||||
const DEFAULT_ENABLED_THEMES = [
|
||||
"general",
|
||||
"social-media",
|
||||
"poster",
|
||||
"music",
|
||||
"video",
|
||||
"novel",
|
||||
];
|
||||
|
||||
// 需要显示创作模式选择器的主题
|
||||
const CREATION_THEMES = [
|
||||
"social-media",
|
||||
@@ -537,6 +496,44 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
onThemeChange,
|
||||
onRecommendationClick,
|
||||
}) => {
|
||||
// 从配置中读取启用的主题
|
||||
const [enabledThemes, setEnabledThemes] = useState<string[]>(
|
||||
DEFAULT_ENABLED_THEMES,
|
||||
);
|
||||
|
||||
// 加载配置
|
||||
useEffect(() => {
|
||||
const loadEnabledThemes = async () => {
|
||||
try {
|
||||
const config = await getConfig();
|
||||
if (config.content_creator?.enabled_themes) {
|
||||
setEnabledThemes(config.content_creator.enabled_themes);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载主题配置失败:", e);
|
||||
}
|
||||
};
|
||||
loadEnabledThemes();
|
||||
|
||||
// 监听主题配置变更事件
|
||||
const handleThemeConfigChange = () => {
|
||||
loadEnabledThemes();
|
||||
};
|
||||
window.addEventListener("theme-config-changed", handleThemeConfigChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"theme-config-changed",
|
||||
handleThemeConfigChange,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 过滤后的主题列表
|
||||
const categories = ALL_CATEGORIES.filter((cat) =>
|
||||
enabledThemes.includes(cat.id),
|
||||
);
|
||||
|
||||
// 使用外部传入的 activeTheme,如果有 onThemeChange 则使用受控模式
|
||||
const handleThemeChange = (theme: string) => {
|
||||
if (onThemeChange) {
|
||||
@@ -552,6 +549,9 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
const [ratio, setRatio] = useState("3:4");
|
||||
const [style, setStyle] = useState("minimal");
|
||||
const [depth, setDepth] = useState("deep");
|
||||
// Popover 打开状态
|
||||
const [ratioPopoverOpen, setRatioPopoverOpen] = useState(false);
|
||||
const [stylePopoverOpen, setStylePopoverOpen] = useState(false);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim()) return;
|
||||
@@ -635,7 +635,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
</Header>
|
||||
|
||||
<TabsContainer>
|
||||
{CATEGORIES.map((cat) => (
|
||||
{categories.map((cat) => (
|
||||
<TabItem
|
||||
key={cat.id}
|
||||
$active={activeTheme === cat.id}
|
||||
@@ -682,7 +682,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
<span>{getPlatformLabel(platform)}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="p-1">
|
||||
<SelectContent className="p-1" side="top">
|
||||
<div className="px-2 py-1.5 text-xs text-muted-foreground font-medium">
|
||||
选择要创作的内容平台
|
||||
</div>
|
||||
@@ -753,7 +753,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
<span>{CREATION_MODE_CONFIG[creationMode].name}</span>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="p-1 min-w-[200px]">
|
||||
<SelectContent className="p-1 min-w-[200px]" side="top">
|
||||
<div className="px-2 py-1.5 text-xs text-muted-foreground font-medium">
|
||||
选择创作模式
|
||||
</div>
|
||||
@@ -788,7 +788,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
<BrainCircuit className="w-3.5 h-3.5 mr-2 text-muted-foreground" />
|
||||
<SelectValue placeholder="深度" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent side="top">
|
||||
<SelectItem value="deep">深度解析</SelectItem>
|
||||
<SelectItem value="quick">快速概览</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -808,7 +808,13 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
|
||||
{activeTheme === "poster" && (
|
||||
<>
|
||||
<Popover>
|
||||
<Popover
|
||||
open={ratioPopoverOpen}
|
||||
onOpenChange={(open) => {
|
||||
setRatioPopoverOpen(open);
|
||||
if (open) setStylePopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -822,7 +828,11 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
<ChevronDown className="w-3 h-3 ml-1 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64 p-2" align="start">
|
||||
<PopoverContent
|
||||
className="w-64 p-2 bg-background border shadow-lg"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<div className="text-xs font-medium mb-2 px-2 text-muted-foreground">
|
||||
宽高比
|
||||
</div>
|
||||
@@ -832,7 +842,10 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
<GridItem
|
||||
key={r}
|
||||
$active={ratio === r}
|
||||
onClick={() => setRatio(r)}
|
||||
onClick={() => {
|
||||
setRatio(r);
|
||||
setRatioPopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-5 h-5 border-2 border-current rounded-sm mb-1 opacity-50"></div>
|
||||
<span className="text-xs">{r}</span>
|
||||
@@ -843,7 +856,13 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Popover>
|
||||
<Popover
|
||||
open={stylePopoverOpen}
|
||||
onOpenChange={(open) => {
|
||||
setStylePopoverOpen(open);
|
||||
if (open) setRatioPopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -859,13 +878,20 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
<ChevronDown className="w-3 h-3 ml-1 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-48 p-1" align="start">
|
||||
<PopoverContent
|
||||
className="w-48 p-1 bg-background border shadow-lg"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
<div className="p-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start h-8"
|
||||
onClick={() => setStyle("minimal")}
|
||||
onClick={() => {
|
||||
setStyle("minimal");
|
||||
setStylePopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<ColorDot $color="#e2e8f0" className="mr-2" />{" "}
|
||||
极简风格
|
||||
@@ -874,7 +900,10 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start h-8"
|
||||
onClick={() => setStyle("tech")}
|
||||
onClick={() => {
|
||||
setStyle("tech");
|
||||
setStylePopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<ColorDot $color="#3b82f6" className="mr-2" />{" "}
|
||||
科技质感
|
||||
@@ -883,7 +912,10 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start h-8"
|
||||
onClick={() => setStyle("warm")}
|
||||
onClick={() => {
|
||||
setStyle("warm");
|
||||
setStylePopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<ColorDot $color="#f59e0b" className="mr-2" />{" "}
|
||||
温暖治愈
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import { GraduationCap, Zap, RefreshCw, LayoutTemplate } from "lucide-react";
|
||||
import type { CreationMode } from "./types";
|
||||
|
||||
/**
|
||||
* 模式配置
|
||||
*/
|
||||
export const CREATION_MODE_CONFIG: Record<
|
||||
CreationMode,
|
||||
{
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
aiRole: string;
|
||||
userInvolvement: "high" | "medium" | "low";
|
||||
description: string;
|
||||
}
|
||||
> = {
|
||||
guided: {
|
||||
name: "引导模式",
|
||||
icon: <GraduationCap className="w-4 h-4" />,
|
||||
aiRole: "教练(提问引导)",
|
||||
userInvolvement: "high",
|
||||
description: "追求真实性、个人经历类内容",
|
||||
},
|
||||
fast: {
|
||||
name: "快速模式",
|
||||
icon: <Zap className="w-4 h-4" />,
|
||||
aiRole: "助手(生成初稿)",
|
||||
userInvolvement: "low",
|
||||
description: "信息整理、快速产出",
|
||||
},
|
||||
hybrid: {
|
||||
name: "混合模式",
|
||||
icon: <RefreshCw className="w-4 h-4" />,
|
||||
aiRole: "协作者(写框架)",
|
||||
userInvolvement: "medium",
|
||||
description: "平衡质量和效率",
|
||||
},
|
||||
framework: {
|
||||
name: "框架模式",
|
||||
icon: <LayoutTemplate className="w-4 h-4" />,
|
||||
aiRole: "填充者(按框架生成)",
|
||||
userInvolvement: "medium",
|
||||
description: "固定格式文档(报告、标书)",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* 创作模式类型
|
||||
* 不同模式下 AI 的角色和用户参与度不同
|
||||
*/
|
||||
export type CreationMode = "guided" | "fast" | "hybrid" | "framework";
|
||||
@@ -16,7 +16,8 @@ import { ChatSidebar } from "./components/ChatSidebar";
|
||||
import { ChatSettings } from "./components/ChatSettings";
|
||||
import { MessageList } from "./components/MessageList";
|
||||
import { Inputbar } from "./components/Inputbar";
|
||||
import { EmptyState, CreationMode } from "./components/EmptyState";
|
||||
import { EmptyState } from "./components/EmptyState";
|
||||
import type { CreationMode } from "./components/types";
|
||||
import { type TaskFile } from "./components/TaskFiles";
|
||||
import { LayoutTransition } from "@/components/content-creator/core/LayoutTransition/LayoutTransition";
|
||||
import { StepProgress } from "@/components/content-creator/core/StepGuide/StepProgress";
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
* 显示项目类型过滤标签
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TYPE_CONFIGS, type UserType } from "@/lib/api/project";
|
||||
import { getConfig } from "@/hooks/useTauri";
|
||||
|
||||
export type ProjectFilter = "all" | UserType | "favorites" | "archived";
|
||||
|
||||
@@ -15,7 +17,17 @@ interface ProjectCategoriesProps {
|
||||
counts?: Record<ProjectFilter, number>;
|
||||
}
|
||||
|
||||
const filterItems: { id: ProjectFilter; label: string; icon?: string }[] = [
|
||||
/** 默认启用的主题 */
|
||||
const DEFAULT_ENABLED_THEMES = [
|
||||
"general",
|
||||
"social-media",
|
||||
"poster",
|
||||
"music",
|
||||
"video",
|
||||
"novel",
|
||||
];
|
||||
|
||||
const allFilterItems: { id: ProjectFilter; label: string; icon?: string }[] = [
|
||||
{ id: "all", label: "全部" },
|
||||
{
|
||||
id: "general",
|
||||
@@ -71,6 +83,51 @@ export function ProjectCategories({
|
||||
onFilterChange,
|
||||
counts,
|
||||
}: ProjectCategoriesProps) {
|
||||
// 从配置中读取启用的主题
|
||||
const [enabledThemes, setEnabledThemes] = useState<string[]>(
|
||||
DEFAULT_ENABLED_THEMES,
|
||||
);
|
||||
|
||||
// 加载配置
|
||||
useEffect(() => {
|
||||
const loadEnabledThemes = async () => {
|
||||
try {
|
||||
const config = await getConfig();
|
||||
if (config.content_creator?.enabled_themes) {
|
||||
setEnabledThemes(config.content_creator.enabled_themes);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载主题配置失败:", e);
|
||||
}
|
||||
};
|
||||
loadEnabledThemes();
|
||||
|
||||
// 监听主题配置变更事件
|
||||
const handleThemeConfigChange = () => {
|
||||
loadEnabledThemes();
|
||||
};
|
||||
window.addEventListener("theme-config-changed", handleThemeConfigChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(
|
||||
"theme-config-changed",
|
||||
handleThemeConfigChange,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 过滤后的标签列表
|
||||
const filterItems = useMemo(() => {
|
||||
return allFilterItems.filter((item) => {
|
||||
// all, favorites, archived 始终显示
|
||||
if (["all", "favorites", "archived"].includes(item.id)) {
|
||||
return true;
|
||||
}
|
||||
// 其他根据配置过滤
|
||||
return enabledThemes.includes(item.id);
|
||||
});
|
||||
}, [enabledThemes]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{filterItems.map((item) => {
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
HeartOff,
|
||||
RotateCcw,
|
||||
Activity,
|
||||
Download,
|
||||
} from "lucide-react";
|
||||
import { useProviderPool } from "@/hooks/useProviderPool";
|
||||
import { useApiKeyProvider } from "@/hooks/useApiKeyProvider";
|
||||
@@ -32,7 +31,6 @@ import { AddCredentialModal } from "./AddCredentialModal";
|
||||
import { EditCredentialModal } from "./EditCredentialModal";
|
||||
import { ErrorDisplay, useErrorDisplay } from "./ErrorDisplay";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import { getConfig } from "@/hooks/useTauri";
|
||||
import { ProviderIcon } from "@/icons/providers";
|
||||
import { ApiKeyProviderSection, AddCustomProviderModal } from "./api-key";
|
||||
import type { ApiKeyProviderSectionRef } from "./api-key";
|
||||
@@ -91,7 +89,8 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [editingCredential, setEditingCredential] =
|
||||
useState<CredentialDisplay | null>(null);
|
||||
const [activeCategory, setActiveCategory] = useState<CategoryType>("oauth");
|
||||
const [activeCategory, setActiveCategory] =
|
||||
useState<CategoryType>("apikey");
|
||||
const [activeTab, setActiveTab] = useState<TabType>("kiro");
|
||||
const [deletingCredentials, setDeletingCredentials] = useState<Set<string>>(
|
||||
new Set(),
|
||||
@@ -121,7 +120,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
|
||||
checkTypeHealth,
|
||||
refreshCredentialToken,
|
||||
updateCredential,
|
||||
migratePrivateConfig,
|
||||
} = useProviderPool();
|
||||
|
||||
// API Key Provider Hook
|
||||
@@ -131,7 +129,7 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
|
||||
refresh: refreshApiKeyProviders,
|
||||
} = useApiKeyProvider();
|
||||
|
||||
const [migrating, setMigrating] = useState(false);
|
||||
const [_migrating, setMigrating] = useState(false);
|
||||
|
||||
// Kiro 本地活跃凭证 UUID
|
||||
const [localActiveUuid, setLocalActiveUuid] = useState<string | null>(null);
|
||||
@@ -241,11 +239,16 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
|
||||
};
|
||||
|
||||
// 迁移 Private 配置到凭证池
|
||||
const handleMigratePrivateConfig = async () => {
|
||||
const _handleMigratePrivateConfig = async () => {
|
||||
setMigrating(true);
|
||||
try {
|
||||
const config = await getConfig();
|
||||
const result = await migratePrivateConfig(config);
|
||||
// const config = await getConfig();
|
||||
// const result = await migratePrivateConfig(config);
|
||||
const result = {
|
||||
migrated_count: 0,
|
||||
skipped_count: 0,
|
||||
errors: [] as string[],
|
||||
};
|
||||
if (result.migrated_count > 0) {
|
||||
showSuccess(
|
||||
`成功迁移 ${result.migrated_count} 个凭证${result.skipped_count > 0 ? `,跳过 ${result.skipped_count} 个已存在的凭证` : ""}`,
|
||||
@@ -351,17 +354,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
|
||||
Provider 后自动使用对应凭证
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleMigratePrivateConfig}
|
||||
disabled={migrating || loading}
|
||||
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm hover:bg-muted disabled:opacity-50"
|
||||
title="从高级设置导入 Private 凭证"
|
||||
>
|
||||
<Download
|
||||
className={`h-4 w-4 ${migrating ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
导入配置
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -372,20 +364,6 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
|
||||
|
||||
{/* Category Tabs - 第一行:分类选择 */}
|
||||
<div className="flex gap-1 mb-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveCategory("oauth");
|
||||
setActiveTab(oauthProviderTypes[0]);
|
||||
}}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${
|
||||
activeCategory === "oauth"
|
||||
? "border-primary bg-primary/10 text-primary"
|
||||
: "border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||
}`}
|
||||
data-testid="oauth-category-tab"
|
||||
>
|
||||
OAuth 凭证
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveCategory("apikey");
|
||||
@@ -439,6 +417,20 @@ export const ProviderPoolPage = forwardRef<ProviderPoolPageRef>(
|
||||
>
|
||||
语音服务
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveCategory("oauth");
|
||||
setActiveTab(oauthProviderTypes[0]);
|
||||
}}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${
|
||||
activeCategory === "oauth"
|
||||
? "border-primary bg-primary/10 text-primary"
|
||||
: "border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted"
|
||||
}`}
|
||||
data-testid="oauth-category-tab"
|
||||
>
|
||||
OAuth 凭证
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* OAuth 凭证分类 - Provider 选择图标网格 */}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
Info,
|
||||
RotateCcw,
|
||||
Volume2,
|
||||
Palette,
|
||||
} from "lucide-react";
|
||||
import { cn, validateProxyUrl } from "@/lib/utils";
|
||||
import { getConfig, saveConfig, Config } from "@/hooks/useTauri";
|
||||
@@ -21,11 +22,61 @@ import { useSoundContext } from "@/contexts/useSoundContext";
|
||||
|
||||
type Theme = "light" | "dark" | "system";
|
||||
|
||||
/** 所有可用的内容创作主题 */
|
||||
const ALL_CONTENT_THEMES = [
|
||||
{ id: "general", label: "通用" },
|
||||
{ id: "social-media", label: "社媒内容" },
|
||||
{ id: "poster", label: "图文海报" },
|
||||
{ id: "music", label: "歌词曲谱" },
|
||||
{ id: "video", label: "短视频" },
|
||||
{ id: "novel", label: "小说" },
|
||||
{ id: "knowledge", label: "知识探索" },
|
||||
{ id: "planning", label: "计划规划" },
|
||||
{ id: "document", label: "办公文档" },
|
||||
] as const;
|
||||
|
||||
/** 默认启用的主题 */
|
||||
const DEFAULT_ENABLED_THEMES = [
|
||||
"general",
|
||||
"social-media",
|
||||
"poster",
|
||||
"music",
|
||||
"video",
|
||||
"novel",
|
||||
];
|
||||
|
||||
/** 所有可用的导航模块 */
|
||||
const ALL_NAV_ITEMS = [
|
||||
{ id: "agent", label: "AI Agent" },
|
||||
{ id: "projects", label: "项目" },
|
||||
{ id: "image-gen", label: "图片生成" },
|
||||
{ id: "api-server", label: "API Server" },
|
||||
{ id: "provider-pool", label: "凭证池" },
|
||||
{ id: "terminal", label: "终端" },
|
||||
{ id: "tools", label: "工具" },
|
||||
{ id: "plugins", label: "插件中心" },
|
||||
] as const;
|
||||
|
||||
/** 默认启用的导航模块 */
|
||||
const DEFAULT_ENABLED_NAV_ITEMS = [
|
||||
"agent",
|
||||
"projects",
|
||||
"image-gen",
|
||||
"api-server",
|
||||
"provider-pool",
|
||||
];
|
||||
|
||||
export function GeneralSettings() {
|
||||
const [theme, setTheme] = useState<Theme>("system");
|
||||
const [launchOnStartup, setLaunchOnStartup] = useState(false);
|
||||
const [minimizeToTray, setMinimizeToTray] = useState(true);
|
||||
const [language, setLanguageState] = useState<Language>("zh");
|
||||
const [enabledThemes, setEnabledThemes] = useState<string[]>(
|
||||
DEFAULT_ENABLED_THEMES,
|
||||
);
|
||||
const [enabledNavItems, setEnabledNavItems] = useState<string[]>(
|
||||
DEFAULT_ENABLED_NAV_ITEMS,
|
||||
);
|
||||
const { resetOnboarding } = useOnboardingState();
|
||||
const { setLanguage: setI18nLanguage } = useI18nPatch();
|
||||
const { soundEnabled, setSoundEnabled, playToolcallSound } =
|
||||
@@ -64,6 +115,12 @@ export function GeneralSettings() {
|
||||
setProxyUrl(c.proxy_url || "");
|
||||
setMinimizeToTray(c.minimize_to_tray ?? true);
|
||||
setLanguageState((c.language || "zh") as Language);
|
||||
setEnabledThemes(
|
||||
c.content_creator?.enabled_themes || DEFAULT_ENABLED_THEMES,
|
||||
);
|
||||
setEnabledNavItems(
|
||||
c.navigation?.enabled_items || DEFAULT_ENABLED_NAV_ITEMS,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("加载配置失败:", e);
|
||||
} finally {
|
||||
@@ -130,6 +187,60 @@ export function GeneralSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
// 切换内容创作主题
|
||||
const handleThemeToggle = async (themeId: string) => {
|
||||
if (!config) return;
|
||||
const newThemes = enabledThemes.includes(themeId)
|
||||
? enabledThemes.filter((t) => t !== themeId)
|
||||
: [...enabledThemes, themeId];
|
||||
|
||||
// 至少保留一个主题
|
||||
if (newThemes.length === 0) return;
|
||||
|
||||
setEnabledThemes(newThemes);
|
||||
try {
|
||||
const newConfig = {
|
||||
...config,
|
||||
content_creator: { enabled_themes: newThemes },
|
||||
};
|
||||
await saveConfig(newConfig);
|
||||
setConfig(newConfig);
|
||||
// 触发内容创作主题更新
|
||||
window.dispatchEvent(new CustomEvent("theme-config-changed"));
|
||||
} catch (err) {
|
||||
console.error("保存主题设置失败:", err);
|
||||
// 回滚
|
||||
setEnabledThemes(enabledThemes);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换导航模块
|
||||
const handleNavItemToggle = async (itemId: string) => {
|
||||
if (!config) return;
|
||||
const newItems = enabledNavItems.includes(itemId)
|
||||
? enabledNavItems.filter((i) => i !== itemId)
|
||||
: [...enabledNavItems, itemId];
|
||||
|
||||
// 至少保留一个模块
|
||||
if (newItems.length === 0) return;
|
||||
|
||||
setEnabledNavItems(newItems);
|
||||
try {
|
||||
const newConfig = {
|
||||
...config,
|
||||
navigation: { enabled_items: newItems },
|
||||
};
|
||||
await saveConfig(newConfig);
|
||||
setConfig(newConfig);
|
||||
// 触发侧边栏更新
|
||||
window.dispatchEvent(new CustomEvent("nav-config-changed"));
|
||||
} catch (err) {
|
||||
console.error("保存导航设置失败:", err);
|
||||
// 回滚
|
||||
setEnabledNavItems(enabledNavItems);
|
||||
}
|
||||
};
|
||||
|
||||
const themeOptions = [
|
||||
{ id: "light" as Theme, label: "浅色", icon: Sun },
|
||||
{ id: "dark" as Theme, label: "深色", icon: Moon },
|
||||
@@ -291,6 +402,64 @@ export function GeneralSettings() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 内容创作主题 */}
|
||||
<div className="rounded-lg border p-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Palette className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">内容创作主题</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
选择在内容创作模式中显示的主题标签
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{ALL_CONTENT_THEMES.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => handleThemeToggle(t.id)}
|
||||
className={cn(
|
||||
"px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
|
||||
enabledThemes.includes(t.id)
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80",
|
||||
)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 导航模块 */}
|
||||
<div className="rounded-lg border p-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Palette className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
<h3 className="text-sm font-medium">导航模块</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
选择在左侧导航栏中显示的功能模块
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{ALL_NAV_ITEMS.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => handleNavItemToggle(item.id)}
|
||||
className={cn(
|
||||
"px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
|
||||
enabledNavItems.includes(item.id)
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-muted text-muted-foreground hover:bg-muted/80",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 重新运行引导 */}
|
||||
<div className="rounded-lg border p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -101,11 +101,13 @@ const SelectValue: React.FC<SelectValueProps> = ({ placeholder }) => {
|
||||
interface SelectContentProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
side?: "top" | "bottom";
|
||||
}
|
||||
|
||||
const SelectContent: React.FC<SelectContentProps> = ({
|
||||
className,
|
||||
children,
|
||||
side = "bottom",
|
||||
}) => {
|
||||
const context = useContext(SelectContext);
|
||||
if (!context) throw new Error("SelectContent must be used within Select");
|
||||
@@ -117,7 +119,8 @@ const SelectContent: React.FC<SelectContentProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full z-50 w-full rounded-md border bg-white shadow-md",
|
||||
"absolute z-50 w-full rounded-md border bg-background shadow-lg",
|
||||
side === "top" ? "bottom-full mb-1" : "top-full mt-1",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -129,6 +129,22 @@ export interface ExperimentalFeatures {
|
||||
screenshot_chat: SmartInputConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容创作配置
|
||||
*/
|
||||
export interface ContentCreatorConfig {
|
||||
/** 启用的主题列表 */
|
||||
enabled_themes: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航栏配置
|
||||
*/
|
||||
export interface NavigationConfig {
|
||||
/** 启用的导航模块列表 */
|
||||
enabled_items: string[];
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
server: {
|
||||
host: string;
|
||||
@@ -173,6 +189,10 @@ export interface Config {
|
||||
language: string;
|
||||
/** 实验室功能配置 */
|
||||
experimental?: ExperimentalFeatures;
|
||||
/** 内容创作配置 */
|
||||
content_creator?: ContentCreatorConfig;
|
||||
/** 导航栏配置 */
|
||||
navigation?: NavigationConfig;
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
|
||||
Reference in New Issue
Block a user