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.
- package/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- 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
|
+
}
|