ideaco 1.1.5

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 (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,187 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { useI18n } from '@/lib/i18n';
6
+
7
+ /**
8
+ * Requirements board page
9
+ * Manage all requirements, displayed by status category
10
+ */
11
+ export default function RequirementsBoard() {
12
+ const { t } = useI18n();
13
+ const { company, fetchRequirements, navigateToRequirement } = useStore();
14
+ const [requirements, setRequirements] = useState([]);
15
+ const [filter, setFilter] = useState('all'); // all | in_progress | completed | failed
16
+
17
+ useEffect(() => {
18
+ fetchRequirements().then(setRequirements);
19
+ }, [company]);
20
+
21
+ // Auto refresh executing requirements
22
+ useEffect(() => {
23
+ const hasRunning = requirements.some(r => r.status === 'in_progress' || r.status === 'planning');
24
+ if (!hasRunning) return;
25
+ const timer = setInterval(() => {
26
+ fetchRequirements().then(setRequirements);
27
+ }, 5000);
28
+ return () => clearInterval(timer);
29
+ }, [requirements]);
30
+
31
+ const filtered = filter === 'all' ? requirements : requirements.filter(r => r.status === filter);
32
+
33
+ const statusCounts = {
34
+ all: requirements.length,
35
+ in_progress: requirements.filter(r => r.status === 'in_progress' || r.status === 'planning' || r.status === 'pending_approval').length,
36
+ completed: requirements.filter(r => r.status === 'completed').length,
37
+ failed: requirements.filter(r => r.status === 'failed').length,
38
+ };
39
+
40
+ const statusConfig = {
41
+ pending: { label: t('requirements.status.pending'), color: 'text-gray-400', bg: 'bg-gray-900/30', icon: '⏳' },
42
+ planning: { label: t('requirements.status.planning'), color: 'text-blue-400', bg: 'bg-blue-900/30', icon: '📝' },
43
+ in_progress: { label: t('requirements.status.in_progress'), color: 'text-yellow-400', bg: 'bg-yellow-900/30', icon: '⚙️' },
44
+ pending_approval: { label: t('requirements.status.pending_approval'), color: 'text-orange-400', bg: 'bg-orange-900/30', icon: '🔍' },
45
+ completed: { label: t('requirements.stats.completed'), color: 'text-green-400', bg: 'bg-green-900/30', icon: '✅' },
46
+ failed: { label: t('requirements.status.failed'), color: 'text-red-400', bg: 'bg-red-900/30', icon: '❌' },
47
+ };
48
+
49
+ return (
50
+ <div className="p-6 space-y-6 animate-fade-in">
51
+ <div>
52
+ <h1 className="text-2xl font-bold">{t('requirements.title')}</h1>
53
+ <p className="text-sm text-[var(--muted)] mt-1">{t('requirements.subtitle')}</p>
54
+ </div>
55
+
56
+ {/* Stats cards */}
57
+ <div className="grid grid-cols-4 gap-4">
58
+ {[
59
+ { key: 'all', label: t('requirements.stats.all'), icon: '📋', color: 'blue' },
60
+ { key: 'in_progress', label: t('requirements.stats.inProgress'), icon: '⚙️', color: 'yellow' },
61
+ { key: 'completed', label: t('requirements.stats.completed'), icon: '✅', color: 'green' },
62
+ { key: 'failed', label: t('requirements.stats.failed'), icon: '❌', color: 'red' },
63
+ ].map(stat => (
64
+ <div
65
+ key={stat.key}
66
+ onClick={() => setFilter(stat.key)}
67
+ className={`card cursor-pointer transition-all ${
68
+ filter === stat.key ? 'ring-1 ring-[var(--accent)]' : ''
69
+ }`}
70
+ >
71
+ <div className="flex items-center justify-between">
72
+ <span className="text-2xl">{stat.icon}</span>
73
+ <span className={`text-3xl font-bold text-${stat.color}-400`}>{statusCounts[stat.key]}</span>
74
+ </div>
75
+ <div className="text-sm text-[var(--muted)] mt-2">{stat.label}</div>
76
+ </div>
77
+ ))}
78
+ </div>
79
+
80
+ {/* Requirements list */}
81
+ {filtered.length === 0 ? (
82
+ <div className="card text-center py-12 text-[var(--muted)] col-span-3">
83
+ <div className="text-5xl mb-4">📋</div>
84
+ <p className="text-lg">{t('requirements.empty')}</p>
85
+ <p className="text-sm mt-1">{t('requirements.emptyHint')}</p>
86
+ </div>
87
+ ) : (
88
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
89
+ {filtered.map(req => {
90
+ const st = statusConfig[req.status] || statusConfig.pending;
91
+ const progress = req.workflow && req.workflow.nodeCount > 0
92
+ ? Math.round((req.workflow.completedCount / req.workflow.nodeCount) * 100)
93
+ : (req.status === 'completed' || req.status === 'pending_approval' ? 100 : 0);
94
+ // SVG ring progress parameters
95
+ const radius = 18;
96
+ const stroke = 3;
97
+ const circumference = 2 * Math.PI * radius;
98
+ const dashOffset = circumference - (progress / 100) * circumference;
99
+ const progressColor =
100
+ req.status === 'completed' ? '#22c55e' :
101
+ req.status === 'pending_approval' ? '#f97316' :
102
+ req.status === 'failed' ? '#ef4444' :
103
+ 'var(--accent)';
104
+
105
+ return (
106
+ <div
107
+ key={req.id}
108
+ className="card cursor-pointer hover:border-[var(--accent)]/30 transition-all flex flex-col"
109
+ onClick={() => navigateToRequirement(req.id)}
110
+ >
111
+ {/* Top: progress ring + title + status */}
112
+ <div className="flex items-center gap-3">
113
+ <div className="shrink-0 relative flex items-center justify-center" style={{ width: 40, height: 40 }}>
114
+ {progress === 100 ? (
115
+ <span className="text-xl">🎉</span>
116
+ ) : (
117
+ <>
118
+ <svg width="40" height="40" className="transform -rotate-90">
119
+ <circle
120
+ cx="20" cy="20" r={radius}
121
+ fill="none"
122
+ stroke="rgba(255,255,255,0.06)"
123
+ strokeWidth={stroke}
124
+ />
125
+ <circle
126
+ cx="20" cy="20" r={radius}
127
+ fill="none"
128
+ stroke={progressColor}
129
+ strokeWidth={stroke}
130
+ strokeLinecap="round"
131
+ strokeDasharray={circumference}
132
+ strokeDashoffset={dashOffset}
133
+ className="transition-all duration-500"
134
+ />
135
+ </svg>
136
+ <span className="absolute text-[10px] font-bold" style={{ color: progressColor }}>
137
+ {`${progress}%`}
138
+ </span>
139
+ </>
140
+ )}
141
+ </div>
142
+ <div className="flex-1 min-w-0">
143
+ <div className="flex items-center gap-2">
144
+ <span className="font-semibold text-sm truncate">{req.title}</span>
145
+ {req.status === 'in_progress' && (
146
+ <span className="animate-pulse text-yellow-400 text-[10px]">⚙️</span>
147
+ )}
148
+ </div>
149
+ <span className={`text-[10px] px-1.5 py-0.5 rounded ${st.bg} ${st.color} inline-block mt-0.5`}>{st.label}</span>
150
+ </div>
151
+ </div>
152
+
153
+ {/* Description (multi-line support) */}
154
+ <p className="text-xs text-[var(--muted)] line-clamp-3 mt-2 leading-relaxed">{req.description}</p>
155
+
156
+ {/* Completion summary */}
157
+ {req.summary && (
158
+ <div className="flex items-center gap-3 text-[10px] text-[var(--muted)] mt-2">
159
+ <span>{t('requirements.summary.success', { n: req.summary.successTasks, total: req.summary.totalTasks })}</span>
160
+ <span>{t('requirements.summary.duration', { n: Math.round((req.summary.totalDuration || 0) / 1000) })}</span>
161
+ </div>
162
+ )}
163
+
164
+ {/* Bottom info */}
165
+ <div className="flex items-center justify-between mt-auto pt-3 border-t border-[var(--border)] text-[10px] text-[var(--muted)]">
166
+ <div className="flex items-center gap-2">
167
+ <span>🏢 {req.departmentName}</span>
168
+ {req.workflow && (
169
+ <span>📊 {req.workflow.completedCount || 0}/{req.workflow.nodeCount || 0}</span>
170
+ )}
171
+ </div>
172
+ <div className="flex items-center gap-2">
173
+ {req.chatCount > 0 && <span>💬 {req.chatCount}</span>}
174
+ {req.outputCount > 0 && <span>📦 {req.outputCount}</span>}
175
+ <span>{new Date(req.createdAt).toLocaleDateString()}</span>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ );
180
+ })}
181
+ </div>
182
+ )}
183
+
184
+ {/* Requirement detail is now a standalone page */}
185
+ </div>
186
+ );
187
+ }
@@ -0,0 +1,295 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { getAvatarChoices } from '@/lib/avatar';
6
+ import { useI18n } from '@/lib/i18n';
7
+ import CachedAvatar from './CachedAvatar';
8
+ import AvatarGrid from './AvatarGrid';
9
+
10
+ export default function SecretarySettings({ onClose }) {
11
+ const { t } = useI18n();
12
+ const { company, updateSecretarySettings } = useStore();
13
+ const [name, setName] = useState('');
14
+ const [prompt, setPrompt] = useState('');
15
+ const [signature, setSignature] = useState('');
16
+ const [providerId, setProviderId] = useState('');
17
+ const [gender, setGender] = useState('female');
18
+ const [age, setAge] = useState(18);
19
+ const [selectedAvatar, setSelectedAvatar] = useState(null); // { url, params }
20
+ const [avatarChoices, setAvatarChoices] = useState([]);
21
+ const [saving, setSaving] = useState(false);
22
+ const [saved, setSaved] = useState(false);
23
+ const [activeTab, setActiveTab] = useState('profile');
24
+
25
+ const secretary = company?.secretary;
26
+
27
+ // Initialize data
28
+ useEffect(() => {
29
+ if (secretary) {
30
+ setName(secretary.name || '');
31
+ setPrompt(secretary.prompt || '');
32
+ setSignature(secretary.signature || '');
33
+ setProviderId(secretary.providerId || '');
34
+ setGender(secretary.gender || 'female');
35
+ setAge(secretary.age || 18);
36
+ }
37
+ }, [secretary]);
38
+
39
+ // Regenerate avatar choices when gender/age changes (debounced)
40
+ const debounceTimer = useRef(null);
41
+ const refreshAvatarDebounced = useCallback((g, a) => {
42
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
43
+ debounceTimer.current = setTimeout(() => {
44
+ const choices = getAvatarChoices(24, g, a);
45
+ setAvatarChoices(choices);
46
+ }, 300);
47
+ }, []);
48
+
49
+ useEffect(() => {
50
+ refreshAvatarDebounced(gender, age);
51
+ return () => { if (debounceTimer.current) clearTimeout(debounceTimer.current); };
52
+ }, [gender, age, refreshAvatarDebounced]);
53
+
54
+ if (!secretary) return null;
55
+
56
+ const previewAvatar = selectedAvatar?.url || secretary.avatar;
57
+
58
+ // Shuffle avatar choices
59
+ const refreshChoices = () => {
60
+ const choices = getAvatarChoices(24, gender, age);
61
+ setAvatarChoices(choices);
62
+ };
63
+
64
+ const handleSave = async () => {
65
+ setSaving(true);
66
+ setSaved(false);
67
+ try {
68
+ const settings = {};
69
+ if (name && name !== secretary.name) settings.name = name;
70
+ if (gender !== secretary.gender) settings.gender = gender;
71
+ if (age !== secretary.age) settings.age = age;
72
+ if (selectedAvatar) {
73
+ settings.avatar = selectedAvatar.url;
74
+ settings.avatarParams = selectedAvatar.params;
75
+ }
76
+ if (prompt !== secretary.prompt) settings.prompt = prompt;
77
+ if (signature && signature !== secretary.signature) settings.signature = signature;
78
+ if (providerId && providerId !== secretary.providerId) settings.providerId = providerId;
79
+
80
+ if (Object.keys(settings).length > 0) {
81
+ await updateSecretarySettings(settings);
82
+ setSaved(true);
83
+ setTimeout(() => setSaved(false), 2000);
84
+ }
85
+ } catch (e) { /* handled */ }
86
+ setSaving(false);
87
+ };
88
+
89
+ return (
90
+ <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 !m-0" onClick={onClose}>
91
+ <div className="card max-w-2xl w-full mx-4 max-h-[92vh] overflow-hidden flex flex-col" onClick={e => e.stopPropagation()}>
92
+ {/* Header */}
93
+ <div className="flex items-center justify-between pb-4 border-b border-[var(--border)]">
94
+ <div className="flex items-center gap-3">
95
+ <CachedAvatar src={previewAvatar} alt="secretary" className="w-14 h-14 rounded-full bg-[var(--border)]" />
96
+ <div>
97
+ <h2 className="text-lg font-bold">{t('secretarySettings.title')}</h2>
98
+ <p className="text-xs text-[var(--muted)]">{t('secretarySettings.subtitle')}</p>
99
+ </div>
100
+ </div>
101
+ <button onClick={onClose} className="text-[var(--muted)] hover:text-white text-xl">✕</button>
102
+ </div>
103
+
104
+ {/* Content area */}
105
+ <div className="flex-1 overflow-auto py-4 space-y-5">
106
+ {/* Tab switcher */}
107
+ <div className="flex gap-1 p-1 rounded-lg bg-[var(--card-bg)] border border-[var(--border)]">
108
+ <button
109
+ onClick={() => setActiveTab('profile')}
110
+ className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-all ${
111
+ activeTab === 'profile'
112
+ ? 'bg-[var(--accent)] text-white shadow-sm'
113
+ : 'text-[var(--muted)] hover:text-white hover:bg-white/5'
114
+ }`}
115
+ >{t('secretarySettings.tabProfile')}</button>
116
+ <button
117
+ onClick={() => setActiveTab('soul')}
118
+ className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-all ${
119
+ activeTab === 'soul'
120
+ ? 'bg-[var(--accent)] text-white shadow-sm'
121
+ : 'text-[var(--muted)] hover:text-white hover:bg-white/5'
122
+ }`}
123
+ >{t('secretarySettings.tabSoul')}</button>
124
+ </div>
125
+
126
+ {activeTab === 'profile' && (<>
127
+ {/* Name */}
128
+ <div>
129
+ <label className="block text-sm font-medium mb-1.5 text-[var(--muted)]">{t('secretarySettings.nameLabel')}</label>
130
+ <input
131
+ className="input w-full"
132
+ value={name}
133
+ onChange={e => setName(e.target.value)}
134
+ placeholder={t('secretarySettings.namePlaceholder')}
135
+ />
136
+ </div>
137
+
138
+ {/* Gender & Age */}
139
+ <div className="grid grid-cols-2 gap-4">
140
+ <div>
141
+ <label className="block text-sm font-medium mb-1.5 text-[var(--muted)]">{t('setup.gender')}</label>
142
+ <div className="flex gap-2">
143
+ <button
144
+ onClick={() => { setGender('female'); setSelectedAvatar(null); }}
145
+ className={`flex-1 py-2 px-3 rounded-lg border text-sm transition-all ${
146
+ gender === 'female'
147
+ ? 'border-pink-400 bg-pink-400/10 text-pink-300'
148
+ : 'border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)]/40'
149
+ }`}
150
+ >{t('setup.female')}</button>
151
+ <button
152
+ onClick={() => { setGender('male'); setSelectedAvatar(null); }}
153
+ className={`flex-1 py-2 px-3 rounded-lg border text-sm transition-all ${
154
+ gender === 'male'
155
+ ? 'border-blue-400 bg-blue-400/10 text-blue-300'
156
+ : 'border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)]/40'
157
+ }`}
158
+ >{t('setup.male')}</button>
159
+ </div>
160
+ </div>
161
+ <div>
162
+ <label className="block text-sm font-medium mb-1.5 text-[var(--muted)]">{t('setup.age', { n: age })}</label>
163
+ <div className="relative flex items-center gap-3">
164
+ <button
165
+ onClick={() => setAge(a => Math.max(18, a - 1))}
166
+ className="w-7 h-7 rounded-full border border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-all flex items-center justify-center text-sm font-bold shrink-0"
167
+ >−</button>
168
+ <div className="flex-1 relative h-5 flex items-center">
169
+ {/* Track background */}
170
+ <div className="absolute left-0 right-0 top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-[var(--border)] pointer-events-none" />
171
+ {/* Gradient progress bar */}
172
+ <div
173
+ className="absolute left-0 top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-gradient-to-r from-[var(--accent)] to-purple-400 pointer-events-none"
174
+ style={{ width: `${((age - 18) / 42) * 100}%` }}
175
+ />
176
+ {/* Range slider input (transparent track, thumb only) */}
177
+ <input
178
+ type="range"
179
+ min="18"
180
+ max="60"
181
+ value={age}
182
+ onChange={e => { setAge(Number(e.target.value)); setSelectedAvatar(null); }}
183
+ className="absolute inset-0 z-10 w-full appearance-none cursor-pointer bg-transparent [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-transparent [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:h-4 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[var(--accent)] [&::-webkit-slider-thumb]:shadow-[0_0_6px_rgba(99,102,241,0.5)] [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:transition-all [&::-webkit-slider-thumb]:hover:scale-125 [&::-webkit-slider-thumb]:-mt-[5px]"
184
+ />
185
+ </div>
186
+ <button
187
+ onClick={() => setAge(a => Math.min(60, a + 1))}
188
+ className="w-7 h-7 rounded-full border border-[var(--border)] text-[var(--muted)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-all flex items-center justify-center text-sm font-bold shrink-0"
189
+ >+</button>
190
+ </div>
191
+ <div className="flex justify-between text-[10px] text-[var(--muted)] mt-1 px-10">
192
+ <span>18</span><span>30</span><span>45</span><span>60</span>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ {/* Avatar selection */}
198
+ <div>
199
+ <div className="flex items-center justify-between mb-1.5">
200
+ <label className="text-sm font-medium text-[var(--muted)]">{t('secretarySettings.avatarStyle')}</label>
201
+ <button
202
+ className="text-xs text-[var(--accent)] hover:underline flex items-center gap-1"
203
+ onClick={refreshChoices}
204
+ >{t('secretarySettings.refreshAvatar')}</button>
205
+ </div>
206
+ {/* Avatar grid */}
207
+ <AvatarGrid
208
+ choices={avatarChoices}
209
+ selectedId={selectedAvatar?.id}
210
+ onSelect={setSelectedAvatar}
211
+ />
212
+ </div>
213
+
214
+ {/* Signature */}
215
+ <div>
216
+ <label className="block text-sm font-medium mb-1.5 text-[var(--muted)]">{t('secretarySettings.signatureLabel')}</label>
217
+ <input
218
+ className="input w-full"
219
+ value={signature}
220
+ onChange={e => setSignature(e.target.value)}
221
+ placeholder={t('secretarySettings.signaturePlaceholder')}
222
+ />
223
+ </div>
224
+ </>)}
225
+
226
+ {activeTab === 'soul' && (<>
227
+ {/* Service provider */}
228
+ <div>
229
+ <label className="block text-sm font-medium mb-1.5 text-[var(--muted)]">{t('secretarySettings.providerLabel')}</label>
230
+ <p className="text-[10px] text-[var(--muted)] mb-2">
231
+ {t('secretarySettings.providerDesc')}
232
+ </p>
233
+ {secretary.availableProviders && secretary.availableProviders.length > 0 ? (
234
+ <select
235
+ className="input w-full"
236
+ value={providerId}
237
+ onChange={e => setProviderId(e.target.value)}
238
+ >
239
+ {secretary.availableProviders.map(p => (
240
+ <option key={p.id} value={p.id}>{p.name}</option>
241
+ ))}
242
+ </select>
243
+ ) : (
244
+ <div className="text-xs text-yellow-400 p-2 rounded bg-yellow-400/10 border border-yellow-400/20">
245
+ {t('secretarySettings.noProviders')}
246
+ </div>
247
+ )}
248
+ </div>
249
+
250
+ {/* Prompt */}
251
+ <div>
252
+ <label className="block text-sm font-medium mb-1.5 text-[var(--muted)]">
253
+ {t('secretarySettings.promptLabel')}
254
+ </label>
255
+ <p className="text-[10px] text-[var(--muted)] mb-2">
256
+ {t('secretarySettings.promptDesc')}
257
+ </p>
258
+ <textarea
259
+ className="input w-full h-48 resize-y text-sm font-mono"
260
+ value={prompt}
261
+ onChange={e => setPrompt(e.target.value)}
262
+ placeholder={t('secretarySettings.promptLabel')}
263
+ />
264
+ <div className="flex items-center justify-between mt-1">
265
+ <span className="text-[10px] text-[var(--muted)]">
266
+ {t('secretarySettings.charCount', { n: prompt.length })}
267
+ </span>
268
+ <button
269
+ className="text-[10px] text-[var(--accent)] hover:underline"
270
+ onClick={() => {
271
+ setPrompt(t('setup.defaultPrompt'));
272
+ }}
273
+ >{t('secretarySettings.restoreDefault')}</button>
274
+ </div>
275
+ </div>
276
+ </>)}
277
+ </div>
278
+
279
+ {/* Footer actions */}
280
+ <div className="pt-4 border-t border-[var(--border)] flex items-center justify-between">
281
+ <div className="text-xs text-[var(--muted)]">
282
+ {t('secretarySettings.modelInfo', { provider: secretary.provider, info: ''})} {secretary.hrAssistant ? t('secretarySettings.withHR') : ''}
283
+ </div>
284
+ <div className="flex items-center gap-2">
285
+ {saved && <span className="text-xs text-green-400 animate-fade-in">{t('secretarySettings.saved')}</span>}
286
+ <button className="btn-secondary" onClick={onClose}>{t('common.cancel')}</button>
287
+ <button className="btn-primary" disabled={saving} onClick={handleSave}>
288
+ {saving ? t('secretarySettings.saving') : t('secretarySettings.saveBtn')}
289
+ </button>
290
+ </div>
291
+ </div>
292
+ </div>
293
+ </div>
294
+ );
295
+ }