@nextclaw/ui 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/index-BIesvTqn.js +225 -0
  3. package/dist/assets/index-iSLahgqA.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/dist/logos/aihubmix.png +0 -0
  6. package/dist/logos/anthropic.svg +1 -0
  7. package/dist/logos/dashscope.png +0 -0
  8. package/dist/logos/deepseek.png +0 -0
  9. package/dist/logos/dingtalk.svg +1 -0
  10. package/dist/logos/discord.svg +1 -0
  11. package/dist/logos/email.svg +1 -0
  12. package/dist/logos/feishu.svg +12 -0
  13. package/dist/logos/gemini.svg +1 -0
  14. package/dist/logos/groq.svg +1 -0
  15. package/dist/logos/minimax.svg +1 -0
  16. package/dist/logos/mochat.svg +6 -0
  17. package/dist/logos/moonshot.png +0 -0
  18. package/dist/logos/openai.svg +1 -0
  19. package/dist/logos/openrouter.svg +1 -0
  20. package/dist/logos/qq.svg +1 -0
  21. package/dist/logos/slack.svg +1 -0
  22. package/dist/logos/telegram.svg +1 -0
  23. package/dist/logos/vllm.svg +1 -0
  24. package/dist/logos/whatsapp.svg +1 -0
  25. package/dist/logos/zhipu.svg +15 -0
  26. package/package.json +1 -1
  27. package/public/logos/aihubmix.png +0 -0
  28. package/public/logos/anthropic.svg +1 -0
  29. package/public/logos/dashscope.png +0 -0
  30. package/public/logos/deepseek.png +0 -0
  31. package/public/logos/dingtalk.svg +1 -0
  32. package/public/logos/discord.svg +1 -0
  33. package/public/logos/email.svg +1 -0
  34. package/public/logos/feishu.svg +12 -0
  35. package/public/logos/gemini.svg +1 -0
  36. package/public/logos/groq.svg +1 -0
  37. package/public/logos/minimax.svg +1 -0
  38. package/public/logos/mochat.svg +6 -0
  39. package/public/logos/moonshot.png +0 -0
  40. package/public/logos/openai.svg +1 -0
  41. package/public/logos/openrouter.svg +1 -0
  42. package/public/logos/qq.svg +1 -0
  43. package/public/logos/slack.svg +1 -0
  44. package/public/logos/telegram.svg +1 -0
  45. package/public/logos/vllm.svg +1 -0
  46. package/public/logos/whatsapp.svg +1 -0
  47. package/public/logos/zhipu.svg +15 -0
  48. package/src/App.tsx +0 -3
  49. package/src/api/config.ts +0 -19
  50. package/src/api/types.ts +5 -8
  51. package/src/components/common/LogoBadge.tsx +35 -0
  52. package/src/components/common/StatusBadge.tsx +4 -4
  53. package/src/components/config/ChannelForm.tsx +16 -18
  54. package/src/components/config/ChannelsList.tsx +87 -37
  55. package/src/components/config/ModelConfig.tsx +25 -25
  56. package/src/components/config/ProviderForm.tsx +46 -11
  57. package/src/components/config/ProvidersList.tsx +90 -38
  58. package/src/components/layout/Header.tsx +7 -7
  59. package/src/components/layout/Sidebar.tsx +10 -23
  60. package/src/components/ui/HighlightCard.tsx +29 -29
  61. package/src/components/ui/button.tsx +13 -8
  62. package/src/components/ui/card.tsx +8 -7
  63. package/src/components/ui/dialog.tsx +8 -8
  64. package/src/components/ui/input.tsx +1 -1
  65. package/src/components/ui/label.tsx +1 -1
  66. package/src/components/ui/switch.tsx +3 -3
  67. package/src/components/ui/tabs-custom.tsx +6 -6
  68. package/src/components/ui/tabs.tsx +7 -6
  69. package/src/hooks/useConfig.ts +2 -29
  70. package/src/index.css +103 -56
  71. package/src/lib/i18n.ts +7 -6
  72. package/src/lib/logos.ts +42 -0
  73. package/src/stores/ui.store.ts +1 -1
  74. package/src/styles/design-system.css +248 -0
  75. package/tailwind.config.js +118 -10
  76. package/dist/assets/index-C4OKhpdC.css +0 -1
  77. package/dist/assets/index-C8nOCIVG.js +0 -240
  78. package/src/components/config/UiConfig.tsx +0 -189
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
9
  <input
10
10
  type={type}
11
11
  className={cn(
12
- 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
12
+ 'flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50',
13
13
  className
14
14
  )}
15
15
  ref={ref}
@@ -8,7 +8,7 @@ const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
8
8
  <label
9
9
  ref={ref}
10
10
  className={cn(
11
- 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
11
+ 'text-sm font-medium leading-none text-gray-700 peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
12
12
  className
13
13
  )}
14
14
  {...props}
@@ -15,8 +15,8 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
15
15
  aria-checked={checked}
16
16
  ref={ref}
17
17
  className={cn(
18
- 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50',
19
- checked ? 'bg-primary' : 'bg-input',
18
+ 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50',
19
+ checked ? 'bg-primary' : 'bg-gray-200 hover:bg-gray-300',
20
20
  className
21
21
  )}
22
22
  onClick={() => onCheckedChange?.(!checked)}
@@ -24,7 +24,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
24
24
  >
25
25
  <span
26
26
  className={cn(
27
- 'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform',
27
+ 'pointer-events-none block h-5 w-5 rounded-full bg-white shadow-md ring-0 transition-transform duration-fast',
28
28
  checked ? 'translate-x-5' : 'translate-x-0'
29
29
  )}
30
30
  />
@@ -16,7 +16,7 @@ interface TabsProps {
16
16
 
17
17
  export function Tabs({ tabs, activeTab, onChange, className }: TabsProps) {
18
18
  return (
19
- <div className={cn('flex items-center gap-8 border-b border-[hsl(40,10%,94%)] mb-8', className)}>
19
+ <div className={cn('flex items-center gap-8 border-b border-gray-200 mb-8', className)}>
20
20
  {tabs.map((tab) => {
21
21
  const isActive = activeTab === tab.id;
22
22
  return (
@@ -24,18 +24,18 @@ export function Tabs({ tabs, activeTab, onChange, className }: TabsProps) {
24
24
  key={tab.id}
25
25
  onClick={() => onChange(tab.id)}
26
26
  className={cn(
27
- 'relative pb-4 text-[15px] font-semibold transition-all duration-200 flex items-center gap-2',
27
+ 'relative pb-4 text-[15px] font-semibold transition-all duration-fast flex items-center gap-2',
28
28
  isActive
29
- ? 'text-[hsl(30,15%,10%)]'
30
- : 'text-[hsl(30,8%,55%)] hover:text-[hsl(30,15%,10%)]'
29
+ ? 'text-primary'
30
+ : 'text-gray-500 hover:text-gray-700'
31
31
  )}
32
32
  >
33
33
  {tab.label}
34
34
  {tab.count !== undefined && (
35
- <span className="text-[11px] font-medium text-[hsl(30,8%,65%)]">{tab.count.toLocaleString()}</span>
35
+ <span className="text-[11px] font-medium text-gray-400">{tab.count.toLocaleString()}</span>
36
36
  )}
37
37
  {isActive && (
38
- <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[hsl(30,15%,10%)] animate-in fade-in slide-in-from-left-2 duration-300" />
38
+ <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary animate-in fade-in slide-in-from-left-2 duration-300" />
39
39
  )}
40
40
  </button>
41
41
  );
@@ -42,7 +42,7 @@ export function TabsList({ children, className }: TabsListProps) {
42
42
  return (
43
43
  <div
44
44
  className={cn(
45
- 'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
45
+ 'inline-flex h-10 items-center justify-center rounded-lg bg-gray-100 p-1 text-gray-600',
46
46
  className
47
47
  )}
48
48
  >
@@ -62,10 +62,10 @@ export function TabsTrigger({ value, children, className }: TabsTriggerProps) {
62
62
  type="button"
63
63
  onClick={() => context.onValueChange(value)}
64
64
  className={cn(
65
- 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
65
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ring-offset-white transition-all duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
66
66
  isActive
67
- ? 'bg-background text-foreground shadow-sm'
68
- : 'hover:bg-background/50',
67
+ ? 'bg-white text-gray-900 shadow-sm'
68
+ : 'hover:bg-white/50 hover:text-gray-700',
69
69
  className
70
70
  )}
71
71
  >
@@ -78,10 +78,11 @@ export function TabsContent({ value, children, className }: TabsContentProps) {
78
78
  const context = React.useContext(TabsContext);
79
79
  if (!context) throw new Error('TabsContent must be used within Tabs');
80
80
 
81
- if (context.value !== value) return null;
81
+ const isActive = context.value === value;
82
+ if (!isActive) return null;
82
83
 
83
84
  return (
84
- <div className={cn('mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', className)}>
85
+ <div className={cn('mt-2 animate-fade-in', className)}>
85
86
  {children}
86
87
  </div>
87
88
  );
@@ -1,5 +1,5 @@
1
1
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
2
- import { fetchConfig, fetchConfigMeta, updateModel, updateProvider, updateChannel, updateUiConfig, reloadConfig } from '@/api/config';
2
+ import { fetchConfig, fetchConfigMeta, updateModel, updateProvider, updateChannel } from '@/api/config';
3
3
  import { toast } from 'sonner';
4
4
  import { t } from '@/lib/i18n';
5
5
 
@@ -59,37 +59,10 @@ export function useUpdateChannel() {
59
59
  updateChannel(channel, data as Parameters<typeof updateChannel>[1]),
60
60
  onSuccess: () => {
61
61
  queryClient.invalidateQueries({ queryKey: ['config'] });
62
- toast.success(t('configSaved'));
63
- },
64
- onError: (error: Error) => {
65
- toast.error(t('configSaveFailed') + ': ' + error.message);
66
- }
67
- });
68
- }
69
-
70
- export function useUpdateUiConfig() {
71
- const queryClient = useQueryClient();
72
-
73
- return useMutation({
74
- mutationFn: updateUiConfig,
75
- onSuccess: () => {
76
- queryClient.invalidateQueries({ queryKey: ['config'] });
77
- toast.success(t('configSaved'));
62
+ toast.success(t('configSavedApplied'));
78
63
  },
79
64
  onError: (error: Error) => {
80
65
  toast.error(t('configSaveFailed') + ': ' + error.message);
81
66
  }
82
67
  });
83
68
  }
84
-
85
- export function useReloadConfig() {
86
- return useMutation({
87
- mutationFn: reloadConfig,
88
- onSuccess: () => {
89
- toast.success(t('configReloaded'));
90
- },
91
- onError: (error: Error) => {
92
- toast.error(t('configReloadFailed') + ': ' + error.message);
93
- }
94
- });
95
- }
package/src/index.css CHANGED
@@ -1,55 +1,67 @@
1
+ /* Import Design System - must be first */
2
+ @import './styles/design-system.css';
3
+
1
4
  @tailwind base;
2
5
  @tailwind components;
3
6
  @tailwind utilities;
4
7
 
5
8
  @layer base {
6
9
  :root {
7
- /* HappyCapy-inspired cleaner "Milk/Cream" palette */
8
- --background: 40 20% 98%;
9
- /* Very light cream #F9F9F7 */
10
- --foreground: 30 15% 10%;
11
- /* Very dark brown/gray */
10
+ /* ========================================
11
+ CORE VARIABLES (Mapped to Design System)
12
+ ======================================== */
13
+ --background: 210 20% 98%;
14
+ --foreground: 221 39% 11%;
15
+
12
16
  --card: 0 0% 100%;
13
- /* Pure white cards */
14
- --card-foreground: 30 15% 10%;
17
+ --card-foreground: 221 39% 11%;
18
+
15
19
  --popover: 0 0% 100%;
16
- --popover-foreground: 30 15% 10%;
17
- --primary: 30 15% 10%;
18
- /* Dark active states */
20
+ --popover-foreground: 221 39% 11%;
21
+
22
+ /* Primary: Brand Blue */
23
+ --primary: 217 80% 55%;
19
24
  --primary-foreground: 0 0% 100%;
20
- --secondary: 40 10% 94%;
21
- /* Neutral light gray */
22
- --secondary-foreground: 30 15% 10%;
23
- --muted: 40 10% 94%;
24
- --muted-foreground: 30 8% 45%;
25
- --accent: 40 10% 92%;
26
- /* Light pill background */
27
- --accent-foreground: 30 15% 10%;
25
+
26
+ /* Secondary: Light Gray */
27
+ --secondary: 220 14% 96%;
28
+ --secondary-foreground: 215 28% 17%;
29
+
30
+ /* Muted */
31
+ --muted: 220 14% 96%;
32
+ --muted-foreground: 220 9% 46%;
33
+
34
+ /* Accent */
35
+ --accent: 217 100% 97%;
36
+ --accent-foreground: 217 70% 40%;
37
+
38
+ /* Destructive */
28
39
  --destructive: 0 84% 60%;
29
40
  --destructive-foreground: 0 0% 98%;
30
- --border: 40 10% 92%;
31
- --input: 40 10% 92%;
32
- --ring: 30 15% 10%;
33
- --radius: 1.25rem;
34
- /* Large rounded corners (20px) */
35
-
36
- /* HappyCapy Neutrals */
37
- --milk-50: 40 20% 98%;
38
- --milk-100: 40 15% 96%;
39
- --milk-200: 40 12% 92%;
40
- --milk-300: 40 10% 88%;
41
- --milk-400: 40 8% 70%;
42
- --milk-500: 30 8% 45%;
43
- --milk-600: 30 10% 35%;
44
- --milk-700: 30 12% 25%;
45
- --milk-800: 30 15% 15%;
46
- --milk-900: 30 20% 8%;
41
+
42
+ /* UI Elements */
43
+ --border: 220 13% 91%;
44
+ --input: 220 13% 91%;
45
+ --ring: 217 80% 55%;
46
+ --radius: 0.75rem;
47
+
48
+ /* Legacy compatibility */
49
+ --milk-50: 210 20% 98%;
50
+ --milk-100: 220 14% 96%;
51
+ --milk-200: 220 13% 91%;
52
+ --milk-300: 216 12% 84%;
53
+ --milk-400: 218 11% 65%;
54
+ --milk-500: 220 9% 46%;
55
+ --milk-600: 215 14% 34%;
56
+ --milk-700: 217 19% 27%;
57
+ --milk-800: 215 28% 17%;
58
+ --milk-900: 221 39% 11%;
47
59
  }
48
60
  }
49
61
 
50
62
  @layer base {
51
63
  * {
52
- @apply border-border outline-none transition-colors duration-200;
64
+ @apply border-border;
53
65
  }
54
66
 
55
67
  html {
@@ -60,20 +72,21 @@
60
72
 
61
73
  body {
62
74
  @apply bg-background text-foreground;
63
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
75
+ font-family: var(--font-sans);
64
76
  overflow: hidden;
65
77
  }
66
78
 
67
- /* Smooth scrolling for the whole page if needed */
79
+ /* Smooth scrolling */
68
80
  * {
69
81
  scrollbar-width: thin;
70
- scrollbar-color: hsl(var(--warm-gray-300)) transparent;
82
+ scrollbar-color: hsl(var(--gray-300)) transparent;
71
83
  }
72
84
  }
73
85
 
74
86
  @layer utilities {
75
-
76
- /* Custom scrollbar */
87
+ /* ========================================
88
+ SCROLLBAR
89
+ ======================================== */
77
90
  .custom-scrollbar::-webkit-scrollbar {
78
91
  width: 6px;
79
92
  height: 6px;
@@ -84,24 +97,42 @@
84
97
  }
85
98
 
86
99
  .custom-scrollbar::-webkit-scrollbar-thumb {
87
- background: hsl(var(--warm-gray-300));
100
+ background: hsl(var(--gray-300));
88
101
  border-radius: 6px;
89
102
  }
90
103
 
91
104
  .custom-scrollbar::-webkit-scrollbar-thumb:hover {
92
- background: hsl(var(--warm-gray-400));
105
+ background: hsl(var(--gray-400));
93
106
  }
94
107
 
95
- /* Glassmorphism */
108
+ /* ========================================
109
+ GLASSMORPHISM
110
+ ======================================== */
96
111
  .glass {
97
- @apply bg-white/70 backdrop-blur-md border border-white/20;
112
+ background: rgba(255, 255, 255, 0.8);
113
+ backdrop-filter: blur(12px);
114
+ -webkit-backdrop-filter: blur(12px);
115
+ border: 1px solid rgba(255, 255, 255, 0.2);
98
116
  }
99
117
 
100
118
  .glass-dark {
101
- @apply bg-black/10 backdrop-blur-md border border-white/10;
119
+ background: rgba(0, 0, 0, 0.1);
120
+ backdrop-filter: blur(12px);
121
+ -webkit-backdrop-filter: blur(12px);
122
+ border: 1px solid rgba(255, 255, 255, 0.1);
123
+ }
124
+
125
+ /* ========================================
126
+ SHADOWS
127
+ ======================================== */
128
+ .shadow-card {
129
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.05), 0 1px 2px -1px rgb(0 0 0 / 0.05);
130
+ }
131
+
132
+ .shadow-card-hover {
133
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.08), 0 4px 6px -4px rgb(0 0 0 / 0.05);
102
134
  }
103
135
 
104
- /* Premium Shadows */
105
136
  .shadow-premium {
106
137
  box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.08), 0 4px 10px -4px rgba(0, 0, 0, 0.04);
107
138
  }
@@ -109,15 +140,36 @@
109
140
  .shadow-premium-hover {
110
141
  box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.12), 0 8px 15px -6px rgba(0, 0, 0, 0.06);
111
142
  }
143
+
144
+ /* ========================================
145
+ GRADIENTS
146
+ ======================================== */
147
+ .bg-gradient-hero {
148
+ background: linear-gradient(180deg, hsl(var(--background)) 0%, hsl(var(--gray-50)) 100%);
149
+ }
150
+
151
+ .bg-gradient-subtle {
152
+ background: linear-gradient(180deg, hsl(var(--gray-50)) 0%, hsl(var(--background)) 100%);
153
+ }
154
+
155
+ /* ========================================
156
+ FOCUS STATES
157
+ ======================================== */
158
+ .focus-ring {
159
+ @apply focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
160
+ --tw-ring-color: hsl(var(--ring));
161
+ --tw-ring-offset-color: hsl(var(--ring-offset));
162
+ }
112
163
  }
113
164
 
114
- /* Animation keyframes */
165
+ /* ========================================
166
+ ANIMATIONS
167
+ ======================================== */
115
168
  @keyframes fadeIn {
116
169
  from {
117
170
  opacity: 0;
118
171
  transform: translateY(12px);
119
172
  }
120
-
121
173
  to {
122
174
  opacity: 1;
123
175
  transform: translateY(0);
@@ -129,7 +181,6 @@
129
181
  opacity: 0;
130
182
  transform: translateX(-12px);
131
183
  }
132
-
133
184
  to {
134
185
  opacity: 1;
135
186
  transform: translateX(0);
@@ -141,7 +192,6 @@
141
192
  opacity: 0;
142
193
  transform: scale(0.97);
143
194
  }
144
-
145
195
  to {
146
196
  opacity: 1;
147
197
  transform: scale(1);
@@ -149,12 +199,9 @@
149
199
  }
150
200
 
151
201
  @keyframes pulse-soft {
152
-
153
- 0%,
154
- 100% {
202
+ 0%, 100% {
155
203
  opacity: 1;
156
204
  }
157
-
158
205
  50% {
159
206
  opacity: 0.8;
160
207
  }
@@ -174,4 +221,4 @@
174
221
 
175
222
  .animate-pulse-soft {
176
223
  animation: pulse-soft 3s ease-in-out infinite;
177
- }
224
+ }
package/src/lib/i18n.ts CHANGED
@@ -4,7 +4,6 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
4
4
  model: { zh: '模型', en: 'Model' },
5
5
  providers: { zh: '提供商', en: 'Providers' },
6
6
  channels: { zh: '渠道', en: 'Channels' },
7
- uiConfig: { zh: '界面', en: 'UI' },
8
7
 
9
8
  // Common
10
9
  enabled: { zh: '启用', en: 'Enabled' },
@@ -29,6 +28,10 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
29
28
  apiKey: { zh: 'API 密钥', en: 'API Key' },
30
29
  apiBase: { zh: 'API Base', en: 'API Base' },
31
30
  extraHeaders: { zh: '额外请求头', en: 'Extra Headers' },
31
+ wireApi: { zh: '请求接口', en: 'Wire API' },
32
+ wireApiAuto: { zh: '自动(优先 Chat,必要时 Responses)', en: 'Auto (Chat with fallback)' },
33
+ wireApiChat: { zh: 'Chat Completions', en: 'Chat Completions' },
34
+ wireApiResponses: { zh: 'Responses', en: 'Responses' },
32
35
  apiKeySet: { zh: '已设置', en: 'Set' },
33
36
  apiKeyNotSet: { zh: '未设置', en: 'Not Set' },
34
37
  showKey: { zh: '显示密钥', en: 'Show Key' },
@@ -41,6 +44,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
41
44
  appToken: { zh: 'App Token', en: 'App Token' },
42
45
  appId: { zh: 'App ID', en: 'App ID' },
43
46
  appSecret: { zh: 'App Secret', en: 'App Secret' },
47
+ markdownSupport: { zh: 'Markdown 支持', en: 'Markdown Support' },
44
48
  clientId: { zh: 'Client ID', en: 'Client ID' },
45
49
  clientSecret: { zh: 'Client Secret', en: 'Client Secret' },
46
50
  encryptKey: { zh: '加密密钥', en: 'Encrypt Key' },
@@ -93,11 +97,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
93
97
  replyDelayMs: { zh: '回复延迟(ms)', en: 'Reply Delay (ms)' },
94
98
  secret: { zh: '密钥', en: 'Secret' },
95
99
 
96
- // UI Config
97
- host: { zh: '主机', en: 'Host' },
98
- port: { zh: '端口', en: 'Port' },
99
- open: { zh: '自动打开', en: 'Open Automatically' },
100
- reloadConfig: { zh: '重载配置', en: 'Reload Config' },
100
+ // UI
101
101
  saveVerifyConnect: { zh: '保存并验证 / 连接', en: 'Save & Verify / Connect' },
102
102
 
103
103
  // Status
@@ -108,6 +108,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
108
108
 
109
109
  // Messages
110
110
  configSaved: { zh: '配置已保存', en: 'Configuration saved' },
111
+ configSavedApplied: { zh: '配置已保存并已应用', en: 'Configuration saved and applied' },
111
112
  configSaveFailed: { zh: '保存配置失败', en: 'Failed to save configuration' },
112
113
  configReloaded: { zh: '配置已重载', en: 'Configuration reloaded' },
113
114
  configReloadFailed: { zh: '重载配置失败', en: 'Failed to reload configuration' },
@@ -0,0 +1,42 @@
1
+ type LogoMap = Record<string, string>;
2
+
3
+ const PROVIDER_LOGOS: LogoMap = {
4
+ openrouter: "openrouter.svg",
5
+ aihubmix: "aihubmix.png",
6
+ anthropic: "anthropic.svg",
7
+ openai: "openai.svg",
8
+ gemini: "gemini.svg",
9
+ deepseek: "deepseek.png",
10
+ zhipu: "zhipu.svg",
11
+ dashscope: "dashscope.png",
12
+ moonshot: "moonshot.png",
13
+ minimax: "minimax.svg",
14
+ vllm: "vllm.svg",
15
+ groq: "groq.svg"
16
+ };
17
+
18
+ const CHANNEL_LOGOS: LogoMap = {
19
+ telegram: "telegram.svg",
20
+ slack: "slack.svg",
21
+ discord: "discord.svg",
22
+ whatsapp: "whatsapp.svg",
23
+ qq: "qq.svg",
24
+ feishu: "feishu.svg",
25
+ dingtalk: "dingtalk.svg",
26
+ mochat: "mochat.svg",
27
+ email: "email.svg"
28
+ };
29
+
30
+ function resolveLogo(map: LogoMap, name: string): string | null {
31
+ const key = name.toLowerCase();
32
+ const file = map[key];
33
+ return file ? `/logos/${file}` : null;
34
+ }
35
+
36
+ export function getProviderLogo(name: string): string | null {
37
+ return resolveLogo(PROVIDER_LOGOS, name);
38
+ }
39
+
40
+ export function getChannelLogo(name: string): string | null {
41
+ return resolveLogo(CHANNEL_LOGOS, name);
42
+ }
@@ -4,7 +4,7 @@ type ConnectionStatus = 'connected' | 'disconnected' | 'connecting';
4
4
 
5
5
  interface UiState {
6
6
  // Active configuration tab
7
- activeTab: 'model' | 'providers' | 'channels' | 'ui';
7
+ activeTab: 'model' | 'providers' | 'channels';
8
8
  setActiveTab: (tab: UiState['activeTab']) => void;
9
9
 
10
10
  // Connection status