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,27 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getCompany } from '@/lib/store';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/ws-files/[departmentId]/file?path=filePath
|
|
6
|
+
* Read a single workspace file content
|
|
7
|
+
*/
|
|
8
|
+
export async function GET(request, { params }) {
|
|
9
|
+
try {
|
|
10
|
+
const company = getCompany();
|
|
11
|
+
if (!company) return NextResponse.json({ error: 'No company' }, { status: 400 });
|
|
12
|
+
|
|
13
|
+
const { departmentId } = await params;
|
|
14
|
+
const url = new URL(request.url);
|
|
15
|
+
const filePath = url.searchParams.get('path');
|
|
16
|
+
|
|
17
|
+
if (!filePath) {
|
|
18
|
+
return NextResponse.json({ error: 'Missing path parameter' }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const content = await company.readWorkspaceFile(departmentId, filePath);
|
|
22
|
+
return NextResponse.json({ data: { path: filePath, content } });
|
|
23
|
+
} catch (e) {
|
|
24
|
+
const status = e.code === 'ENOENT' ? 404 : e.code === 'EACCES' ? 403 : 500;
|
|
25
|
+
return NextResponse.json({ error: e.message }, { status });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { getCompany } from '@/lib/store';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/ws-files/[departmentId]/files?path=subPath
|
|
6
|
+
* List workspace files (shallow, one level)
|
|
7
|
+
*/
|
|
8
|
+
export async function GET(request, { params }) {
|
|
9
|
+
try {
|
|
10
|
+
const company = getCompany();
|
|
11
|
+
if (!company) return NextResponse.json({ error: 'No company' }, { status: 400 });
|
|
12
|
+
|
|
13
|
+
const { departmentId } = await params;
|
|
14
|
+
const url = new URL(request.url);
|
|
15
|
+
const subPath = url.searchParams.get('path') || '';
|
|
16
|
+
|
|
17
|
+
const files = await company.getShallowWorkspaceFiles(departmentId, subPath);
|
|
18
|
+
return NextResponse.json({ data: files });
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
:root {
|
|
6
|
+
--background: #0a0a0a;
|
|
7
|
+
--foreground: #ededed;
|
|
8
|
+
--card: #141414;
|
|
9
|
+
--card-hover: #1a1a1a;
|
|
10
|
+
--border: #2a2a2a;
|
|
11
|
+
--accent: #3b82f6;
|
|
12
|
+
--accent-hover: #2563eb;
|
|
13
|
+
--success: #22c55e;
|
|
14
|
+
--warning: #f59e0b;
|
|
15
|
+
--danger: #ef4444;
|
|
16
|
+
--muted: #71717a;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
background: var(--background);
|
|
21
|
+
color: var(--foreground);
|
|
22
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Electron: macOS hiddenInset ๆ ้ขๆ ๆๆฝๅบๅ */
|
|
26
|
+
.electron-drag {
|
|
27
|
+
-webkit-app-region: drag;
|
|
28
|
+
}
|
|
29
|
+
.electron-no-drag {
|
|
30
|
+
-webkit-app-region: no-drag;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Electron macOS: ๆ ้ขๆ ๅ็งป้ */
|
|
34
|
+
:root {
|
|
35
|
+
--titlebar-height: 0px;
|
|
36
|
+
}
|
|
37
|
+
.electron-mac {
|
|
38
|
+
--titlebar-height: 36px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Global scrollbar hiding */
|
|
42
|
+
::-webkit-scrollbar {
|
|
43
|
+
width: 0;
|
|
44
|
+
height: 0;
|
|
45
|
+
display: none;
|
|
46
|
+
}
|
|
47
|
+
* {
|
|
48
|
+
-ms-overflow-style: none;
|
|
49
|
+
scrollbar-width: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Animations */
|
|
53
|
+
@keyframes fadeIn {
|
|
54
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
55
|
+
to { opacity: 1; transform: translateY(0); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@keyframes slideIn {
|
|
59
|
+
from { opacity: 0; transform: translateX(-20px); }
|
|
60
|
+
to { opacity: 1; transform: translateX(0); }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@keyframes pulse-glow {
|
|
64
|
+
0%, 100% { box-shadow: 0 0 5px rgba(59, 130, 246, 0.3); }
|
|
65
|
+
50% { box-shadow: 0 0 20px rgba(59, 130, 246, 0.6); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.animate-fade-in {
|
|
69
|
+
animation: fadeIn 0.3s ease-out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.animate-slide-in {
|
|
73
|
+
animation: slideIn 0.3s ease-out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.animate-pulse-glow {
|
|
77
|
+
animation: pulse-glow 2s ease-in-out infinite;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Card styles */
|
|
81
|
+
.card {
|
|
82
|
+
@apply bg-[var(--card)] border border-[var(--border)] rounded-xl p-4 transition-all duration-200;
|
|
83
|
+
}
|
|
84
|
+
.card:hover {
|
|
85
|
+
background: var(--card-hover);
|
|
86
|
+
border-color: rgba(59, 130, 246, 0.3);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Button styles */
|
|
90
|
+
.btn-primary {
|
|
91
|
+
@apply bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.btn-secondary {
|
|
95
|
+
@apply bg-[var(--card)] hover:bg-[var(--card-hover)] border border-[var(--border)] text-[var(--foreground)] px-4 py-2 rounded-lg font-medium transition-all duration-200;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.btn-danger {
|
|
99
|
+
@apply bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* === Design Spec: Button Width ===
|
|
104
|
+
* Form submit buttons inside card/form areas should NOT stretch to full width (w-full) on desktop.
|
|
105
|
+
* Use `max-w-xs` (max-width: 20rem / 320px) to keep buttons compact and visually balanced.
|
|
106
|
+
* Example: <button className="btn-primary max-w-xs">Submit</button>
|
|
107
|
+
* Full-width buttons are only acceptable in narrow contexts (modals โค 400px, mobile viewports).
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/* Input */
|
|
111
|
+
.input {
|
|
112
|
+
@apply bg-[var(--card)] border border-[var(--border)] rounded-lg px-3 py-2 text-[var(--foreground)] focus:border-[var(--accent)] focus:outline-none focus:ring-1 focus:ring-[var(--accent)] transition-all duration-200;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Status indicators */
|
|
116
|
+
.status-dot {
|
|
117
|
+
@apply w-2 h-2 rounded-full inline-block;
|
|
118
|
+
}
|
|
119
|
+
.status-dot.active {
|
|
120
|
+
@apply bg-green-500;
|
|
121
|
+
}
|
|
122
|
+
.status-dot.idle {
|
|
123
|
+
@apply bg-gray-500;
|
|
124
|
+
}
|
|
125
|
+
.status-dot.working {
|
|
126
|
+
@apply bg-yellow-500 animate-pulse;
|
|
127
|
+
}
|
|
128
|
+
.status-dot.dismissed {
|
|
129
|
+
@apply bg-red-500;
|
|
130
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import './globals.css';
|
|
5
|
+
import { I18nProvider } from '@/lib/i18n';
|
|
6
|
+
|
|
7
|
+
export default function RootLayout({ children }) {
|
|
8
|
+
const [isElectronMac, setIsElectronMac] = useState(false);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (window.electronAPI?.isElectron && window.electronAPI.platform === 'darwin') {
|
|
12
|
+
setIsElectronMac(true);
|
|
13
|
+
}
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<html lang="en" suppressHydrationWarning>
|
|
18
|
+
<head>
|
|
19
|
+
<title>AI Enterprise - AI Company Management</title>
|
|
20
|
+
<meta name="description" content="Recruit AI Agents to form departments and collaborate on real projects" />
|
|
21
|
+
</head>
|
|
22
|
+
<body className={`min-h-screen ${isElectronMac ? 'electron-mac' : ''}`}>
|
|
23
|
+
{isElectronMac && (
|
|
24
|
+
<div
|
|
25
|
+
style={{
|
|
26
|
+
position: 'fixed',
|
|
27
|
+
top: 0,
|
|
28
|
+
left: 0,
|
|
29
|
+
right: 0,
|
|
30
|
+
height: 36,
|
|
31
|
+
zIndex: 9999,
|
|
32
|
+
WebkitAppRegion: 'drag',
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
)}
|
|
36
|
+
<I18nProvider>{children}</I18nProvider>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
|
39
|
+
);
|
|
40
|
+
}
|
package/src/app/page.jsx
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useStore } from '@/lib/client-store';
|
|
5
|
+
import { useI18n } from '@/lib/i18n';
|
|
6
|
+
import SetupWizard from '@/components/SetupWizard';
|
|
7
|
+
import OnboardingGuide from '@/components/OnboardingGuide';
|
|
8
|
+
import Sidebar from '@/components/Sidebar';
|
|
9
|
+
import Overview from '@/components/Overview';
|
|
10
|
+
import DepartmentView from '@/components/DepartmentView';
|
|
11
|
+
import DepartmentDetail from '@/components/DepartmentDetail';
|
|
12
|
+
import Mailbox from '@/components/Mailbox';
|
|
13
|
+
import MessagesView from '@/components/MessagesView';
|
|
14
|
+
import RequirementsBoard from '@/components/RequirementsBoard';
|
|
15
|
+
import RequirementDetail from '@/components/RequirementDetail';
|
|
16
|
+
import TeamDetail from '@/components/TeamDetail';
|
|
17
|
+
import ChatPanel from '@/components/ChatPanel';
|
|
18
|
+
import PixelOffice from '@/components/PixelOffice';
|
|
19
|
+
|
|
20
|
+
const ONBOARDING_KEY = 'ideaco-onboarding-done';
|
|
21
|
+
|
|
22
|
+
export default function Home() {
|
|
23
|
+
const { company, initialized, activeTab, fetchCompany, error, clearError } = useStore();
|
|
24
|
+
const { t } = useI18n();
|
|
25
|
+
const [showOnboarding, setShowOnboarding] = useState(false);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
fetchCompany();
|
|
29
|
+
}, [fetchCompany]);
|
|
30
|
+
|
|
31
|
+
// Show onboarding when company is loaded and hasn't completed it yet
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!company) return;
|
|
34
|
+
const doneKey = `${ONBOARDING_KEY}-${company.id}`;
|
|
35
|
+
const done = typeof localStorage !== 'undefined' && localStorage.getItem(doneKey);
|
|
36
|
+
if (!done) {
|
|
37
|
+
setShowOnboarding(true);
|
|
38
|
+
}
|
|
39
|
+
}, [company]);
|
|
40
|
+
|
|
41
|
+
const handleOnboardingComplete = () => {
|
|
42
|
+
setShowOnboarding(false);
|
|
43
|
+
if (company?.id) {
|
|
44
|
+
try {
|
|
45
|
+
localStorage.setItem(`${ONBOARDING_KEY}-${company.id}`, '1');
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Initialization request not yet complete, show loading screen instead of SetupWizard
|
|
51
|
+
if (!initialized) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
54
|
+
<div className="text-center animate-pulse">
|
|
55
|
+
<div className="text-6xl mb-4">๐ข</div>
|
|
56
|
+
<p className="text-[var(--muted)]">{t('loadingScreen.text')}</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!company) {
|
|
63
|
+
return <SetupWizard />;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const renderContent = () => {
|
|
67
|
+
switch (activeTab) {
|
|
68
|
+
case 'overview': return <Overview />;
|
|
69
|
+
case 'requirements': return <RequirementsBoard />;
|
|
70
|
+
case 'requirement-detail': return <RequirementDetail />;
|
|
71
|
+
case 'departments': return <DepartmentView />;
|
|
72
|
+
case 'department-detail': return <DepartmentDetail />;
|
|
73
|
+
case 'team-detail': return <TeamDetail />;
|
|
74
|
+
case 'mailbox': return <Mailbox />;
|
|
75
|
+
case 'messages': return <MessagesView />;
|
|
76
|
+
case 'office': return <PixelOffice />;
|
|
77
|
+
default: return <Overview />;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex h-screen">
|
|
83
|
+
<Sidebar />
|
|
84
|
+
<main className="flex-1 overflow-auto relative">
|
|
85
|
+
{error && (
|
|
86
|
+
<div className="bg-red-900/30 border border-red-500/50 text-red-300 px-4 py-3 m-4 rounded-lg flex justify-between items-center animate-fade-in">
|
|
87
|
+
<span>๐ {error}</span>
|
|
88
|
+
<button onClick={clearError} className="text-red-300 hover:text-red-100 ml-4">โ</button>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
{renderContent()}
|
|
92
|
+
</main>
|
|
93
|
+
<ChatPanel />
|
|
94
|
+
{showOnboarding && <OnboardingGuide onComplete={handleOnboardingComplete} />}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { useStore } from '@/lib/client-store';
|
|
5
|
+
import { useI18n } from '@/lib/i18n';
|
|
6
|
+
import { MessageBubble, ChatInput, TaskStatusPanel } from './ChatShared';
|
|
7
|
+
import CachedAvatar from './CachedAvatar';
|
|
8
|
+
|
|
9
|
+
export default function AgentChatModal({ agentId, agentName, agentAvatar, agentRole, agentSignature, agentDepartment, onClose }) {
|
|
10
|
+
const { t } = useI18n();
|
|
11
|
+
const { chatWithAgent, fetchAgentChatHistory, company } = useStore();
|
|
12
|
+
const [messages, setMessages] = useState([]);
|
|
13
|
+
const [input, setInput] = useState('');
|
|
14
|
+
const [sending, setSending] = useState(false);
|
|
15
|
+
const [loadingHistory, setLoadingHistory] = useState(true);
|
|
16
|
+
const messagesEndRef = useRef(null);
|
|
17
|
+
const inputRef = useRef(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
(async () => {
|
|
21
|
+
setLoadingHistory(true);
|
|
22
|
+
try {
|
|
23
|
+
const history = await fetchAgentChatHistory(agentId);
|
|
24
|
+
setMessages(history);
|
|
25
|
+
} catch (e) { /* handled */ }
|
|
26
|
+
setLoadingHistory(false);
|
|
27
|
+
})();
|
|
28
|
+
}, [agentId, fetchAgentChatHistory]);
|
|
29
|
+
|
|
30
|
+
const scrollToBottom = useCallback(() => {
|
|
31
|
+
requestAnimationFrame(() => {
|
|
32
|
+
requestAnimationFrame(() => {
|
|
33
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
inputRef.current?.focus();
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const handleSend = useCallback(async () => {
|
|
43
|
+
if (!input.trim() || sending) return;
|
|
44
|
+
const userMessage = input.trim();
|
|
45
|
+
setInput('');
|
|
46
|
+
setSending(true);
|
|
47
|
+
|
|
48
|
+
const optimisticMsg = { role: 'boss', content: userMessage, time: new Date().toISOString() };
|
|
49
|
+
setMessages(prev => [...prev, optimisticMsg]);
|
|
50
|
+
scrollToBottom();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const data = await chatWithAgent(agentId, userMessage);
|
|
54
|
+
if (data.chatHistory) {
|
|
55
|
+
setMessages(data.chatHistory);
|
|
56
|
+
} else if (data.reply) {
|
|
57
|
+
setMessages(prev => [...prev, {
|
|
58
|
+
role: 'agent',
|
|
59
|
+
content: data.reply.reply,
|
|
60
|
+
time: data.reply.time,
|
|
61
|
+
}]);
|
|
62
|
+
}
|
|
63
|
+
} catch (e) {
|
|
64
|
+
setMessages(prev => [...prev, {
|
|
65
|
+
role: 'agent',
|
|
66
|
+
content: `๐ต ${t('agentChat.error')}: ${e.message}`,
|
|
67
|
+
time: new Date().toISOString(),
|
|
68
|
+
}]);
|
|
69
|
+
}
|
|
70
|
+
setSending(false);
|
|
71
|
+
inputRef.current?.focus();
|
|
72
|
+
}, [input, sending, agentId, chatWithAgent, t]);
|
|
73
|
+
|
|
74
|
+
const handleKeyDown = (e) => {
|
|
75
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
handleSend();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const bossAvatar = company?.bossAvatar;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-[70] !m-0" onClick={onClose}>
|
|
85
|
+
<div className="card max-w-lg w-full mx-4 h-[70vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
|
86
|
+
{/* Header */}
|
|
87
|
+
<div className="flex items-center gap-3 -mx-4 -mt-4 px-4 py-3 border-b border-[var(--border)] shrink-0">
|
|
88
|
+
<CachedAvatar src={agentAvatar} alt={agentName} className="w-10 h-10 rounded-full bg-[var(--border)]" />
|
|
89
|
+
<div className="flex-1 min-w-0">
|
|
90
|
+
<div className="text-sm font-semibold flex items-center gap-2">
|
|
91
|
+
{agentName}
|
|
92
|
+
<span className="w-2 h-2 bg-green-500 rounded-full" />
|
|
93
|
+
{(agentRole || agentDepartment) && (
|
|
94
|
+
<span className="text-[10px] text-[var(--muted)] font-normal">
|
|
95
|
+
{agentRole}{agentDepartment ? ` ยท ${agentDepartment}` : ''}
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
{agentSignature ? (
|
|
100
|
+
<div className="text-[10px] text-[var(--muted)] italic truncate" title={agentSignature}>"{agentSignature}"</div>
|
|
101
|
+
) : (
|
|
102
|
+
<div className="text-[10px] text-[var(--muted)]">{t('agentChat.empty', { name: agentName })}</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
<button onClick={onClose} className="text-[var(--muted)] hover:text-white text-xl w-7 h-7 flex items-center justify-center rounded-lg hover:bg-white/10 transition-all">โ</button>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Messages - reuse MessageBubble */}
|
|
109
|
+
<div className="flex-1 overflow-auto py-3 space-y-3">
|
|
110
|
+
{loadingHistory ? (
|
|
111
|
+
<div className="text-center text-[var(--muted)] py-8">
|
|
112
|
+
<div className="text-2xl animate-pulse">๐ฌ</div>
|
|
113
|
+
<p className="text-xs mt-2">{t('common.loading')}</p>
|
|
114
|
+
</div>
|
|
115
|
+
) : messages.length === 0 ? (
|
|
116
|
+
<div className="text-center text-[var(--muted)] py-8">
|
|
117
|
+
<div className="text-3xl">๐</div>
|
|
118
|
+
<p className="text-sm mt-2">{t('agentChat.empty', { name: agentName })}</p>
|
|
119
|
+
</div>
|
|
120
|
+
) : (
|
|
121
|
+
messages.map((msg, i) => (
|
|
122
|
+
<MessageBubble
|
|
123
|
+
key={i}
|
|
124
|
+
isMe={msg.role === 'boss'}
|
|
125
|
+
avatar={msg.role !== 'boss' ? agentAvatar : null}
|
|
126
|
+
name={msg.role === 'boss' ? (company?.boss || 'Boss') : agentName}
|
|
127
|
+
content={msg.content}
|
|
128
|
+
time={msg.time}
|
|
129
|
+
agentId={null}
|
|
130
|
+
onClickAvatar={null}
|
|
131
|
+
bossAvatar={bossAvatar}
|
|
132
|
+
/>
|
|
133
|
+
))
|
|
134
|
+
)}
|
|
135
|
+
{sending && (
|
|
136
|
+
<div className="flex gap-2">
|
|
137
|
+
<CachedAvatar src={agentAvatar} alt={agentName} className="w-7 h-7 rounded-full bg-[var(--border)] shrink-0 mt-0.5" />
|
|
138
|
+
<div className="bg-[var(--card)] border border-[var(--border)] rounded-2xl rounded-bl-sm px-3 py-2 text-sm">
|
|
139
|
+
<span className="animate-pulse text-[var(--muted)]">{t('agentChat.typing')}</span>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
<div ref={messagesEndRef} />
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Task status panel - shared */}
|
|
147
|
+
<TaskStatusPanel />
|
|
148
|
+
|
|
149
|
+
{/* Input - reuse ChatInput, with negative margin to match card padding */}
|
|
150
|
+
<div className="-mx-4 -mb-4">
|
|
151
|
+
<ChatInput
|
|
152
|
+
value={input}
|
|
153
|
+
onChange={setInput}
|
|
154
|
+
onSend={handleSend}
|
|
155
|
+
onKeyDown={handleKeyDown}
|
|
156
|
+
sending={sending}
|
|
157
|
+
placeholder={t('agentChat.inputPlaceholder', { name: agentName })}
|
|
158
|
+
inputRef={inputRef}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|