@hanzo/rooms 0.1.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.
- package/LICENSE-APACHE +205 -0
- package/LICENSE-MIT +21 -0
- package/README.md +23 -0
- package/dist/Account.d.ts +8 -0
- package/dist/Account.js +58 -0
- package/dist/Beluga.d.ts +12 -0
- package/dist/Beluga.js +152 -0
- package/dist/Billing.d.ts +1 -0
- package/dist/Billing.js +31 -0
- package/dist/Board.d.ts +9 -0
- package/dist/Board.js +447 -0
- package/dist/Boxes.d.ts +1 -0
- package/dist/Boxes.js +173 -0
- package/dist/CalScheduler.d.ts +15 -0
- package/dist/CalScheduler.js +196 -0
- package/dist/Chat.d.ts +35 -0
- package/dist/Chat.js +1049 -0
- package/dist/Column.d.ts +15 -0
- package/dist/Column.js +88 -0
- package/dist/Contacts.d.ts +1 -0
- package/dist/Contacts.js +301 -0
- package/dist/Directory.d.ts +24 -0
- package/dist/Directory.js +356 -0
- package/dist/Drive.d.ts +2 -0
- package/dist/Drive.js +466 -0
- package/dist/Find.d.ts +7 -0
- package/dist/Find.js +82 -0
- package/dist/Guide.d.ts +6 -0
- package/dist/Guide.js +272 -0
- package/dist/Home.d.ts +39 -0
- package/dist/Home.js +306 -0
- package/dist/Inbox.d.ts +9 -0
- package/dist/Inbox.js +193 -0
- package/dist/Market.d.ts +3 -0
- package/dist/Market.js +149 -0
- package/dist/Me.d.ts +27 -0
- package/dist/Me.js +251 -0
- package/dist/Meet.d.ts +9 -0
- package/dist/Meet.js +370 -0
- package/dist/ModeMark.d.ts +8 -0
- package/dist/ModeMark.js +18 -0
- package/dist/OrgSwitcher.d.ts +31 -0
- package/dist/OrgSwitcher.js +120 -0
- package/dist/Orgs.d.ts +10 -0
- package/dist/Orgs.js +582 -0
- package/dist/Profile.d.ts +4 -0
- package/dist/Profile.js +67 -0
- package/dist/Prose.d.ts +5 -0
- package/dist/Prose.js +111 -0
- package/dist/ProviderMark.d.ts +17 -0
- package/dist/ProviderMark.js +277 -0
- package/dist/Recents.d.ts +10 -0
- package/dist/Recents.js +91 -0
- package/dist/RightPane.d.ts +13 -0
- package/dist/RightPane.js +426 -0
- package/dist/Room.d.ts +8 -0
- package/dist/Room.js +50 -0
- package/dist/Scene.d.ts +12 -0
- package/dist/Scene.js +105 -0
- package/dist/Settings.d.ts +46 -0
- package/dist/Settings.js +321 -0
- package/dist/Setup.d.ts +1 -0
- package/dist/Setup.js +36 -0
- package/dist/Share.d.ts +3 -0
- package/dist/Share.js +130 -0
- package/dist/Shared.d.ts +1 -0
- package/dist/Shared.js +113 -0
- package/dist/Shell.d.ts +63 -0
- package/dist/Shell.js +1225 -0
- package/dist/Start.d.ts +1 -0
- package/dist/Start.js +96 -0
- package/dist/Support.d.ts +14 -0
- package/dist/Support.js +122 -0
- package/dist/Talk.d.ts +4 -0
- package/dist/Talk.js +107 -0
- package/dist/Upgrade.d.ts +11 -0
- package/dist/Upgrade.js +75 -0
- package/dist/Usage.d.ts +1 -0
- package/dist/Usage.js +21 -0
- package/dist/Vibe.d.ts +2 -0
- package/dist/Vibe.js +188 -0
- package/dist/Work.d.ts +12 -0
- package/dist/Work.js +256 -0
- package/dist/bots/Bots.d.ts +2 -0
- package/dist/bots/Bots.js +456 -0
- package/dist/bots/Lights.d.ts +8 -0
- package/dist/bots/Lights.js +13 -0
- package/dist/bots/api.d.ts +120 -0
- package/dist/bots/api.js +75 -0
- package/dist/bots/face.d.ts +5 -0
- package/dist/bots/face.js +13 -0
- package/dist/bots/ink.d.ts +7 -0
- package/dist/bots/ink.js +29 -0
- package/dist/bots/kit.d.ts +113 -0
- package/dist/bots/kit.js +43 -0
- package/dist/bots/runtime.d.ts +4 -0
- package/dist/bots/runtime.js +326 -0
- package/dist/card.d.ts +52 -0
- package/dist/card.js +52 -0
- package/dist/cast.d.ts +143 -0
- package/dist/cast.js +251 -0
- package/dist/connect.d.ts +29 -0
- package/dist/connect.js +64 -0
- package/dist/contacts-store.d.ts +111 -0
- package/dist/contacts-store.js +276 -0
- package/dist/conversations.d.ts +54 -0
- package/dist/conversations.js +98 -0
- package/dist/copy.d.ts +23 -0
- package/dist/copy.js +39 -0
- package/dist/crew.d.ts +36 -0
- package/dist/crew.js +304 -0
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +102 -0
- package/dist/enso.d.ts +70 -0
- package/dist/enso.js +112 -0
- package/dist/failure.d.ts +57 -0
- package/dist/failure.js +143 -0
- package/dist/ground.d.ts +32 -0
- package/dist/ground.js +32 -0
- package/dist/host.d.ts +76 -0
- package/dist/host.js +76 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/kept.d.ts +5 -0
- package/dist/kept.js +27 -0
- package/dist/lib/account.d.ts +49 -0
- package/dist/lib/account.js +229 -0
- package/dist/lib/ai.d.ts +46 -0
- package/dist/lib/ai.js +140 -0
- package/dist/lib/api.d.ts +43 -0
- package/dist/lib/api.js +62 -0
- package/dist/lib/bare.d.ts +9 -0
- package/dist/lib/bare.js +7 -0
- package/dist/lib/coding.d.ts +105 -0
- package/dist/lib/coding.js +87 -0
- package/dist/lib/destination.d.ts +102 -0
- package/dist/lib/destination.js +260 -0
- package/dist/lib/durable.d.ts +66 -0
- package/dist/lib/durable.js +107 -0
- package/dist/lib/first.d.ts +11 -0
- package/dist/lib/first.js +23 -0
- package/dist/lib/home.d.ts +61 -0
- package/dist/lib/home.js +54 -0
- package/dist/lib/host.d.ts +104 -0
- package/dist/lib/host.js +132 -0
- package/dist/lib/hostname.d.ts +1 -0
- package/dist/lib/hostname.js +14 -0
- package/dist/lib/hydrated.d.ts +1 -0
- package/dist/lib/hydrated.js +20 -0
- package/dist/lib/integrations.d.ts +21 -0
- package/dist/lib/integrations.js +1 -0
- package/dist/lib/intent.d.ts +6 -0
- package/dist/lib/intent.js +71 -0
- package/dist/lib/invite.d.ts +155 -0
- package/dist/lib/invite.js +245 -0
- package/dist/lib/limits.d.ts +55 -0
- package/dist/lib/limits.js +119 -0
- package/dist/lib/list.d.ts +9 -0
- package/dist/lib/list.js +11 -0
- package/dist/lib/mark.d.ts +13 -0
- package/dist/lib/mark.js +85 -0
- package/dist/lib/mix.d.ts +22 -0
- package/dist/lib/mix.js +22 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +106 -0
- package/dist/lib/org.d.ts +109 -0
- package/dist/lib/org.js +140 -0
- package/dist/lib/path.d.ts +9 -0
- package/dist/lib/path.js +29 -0
- package/dist/lib/plans.d.ts +193 -0
- package/dist/lib/plans.js +310 -0
- package/dist/lib/prose.d.ts +13 -0
- package/dist/lib/prose.js +62 -0
- package/dist/lib/reach.d.ts +17 -0
- package/dist/lib/reach.js +70 -0
- package/dist/lib/rooms.json +23 -0
- package/dist/lib/served.d.ts +20 -0
- package/dist/lib/served.js +92 -0
- package/dist/lib/session.d.ts +211 -0
- package/dist/lib/session.js +493 -0
- package/dist/lib/share.d.ts +131 -0
- package/dist/lib/share.js +216 -0
- package/dist/lib/signout.d.ts +14 -0
- package/dist/lib/signout.js +40 -0
- package/dist/lib/tags.d.ts +27 -0
- package/dist/lib/tags.js +46 -0
- package/dist/lib/tap.d.ts +17 -0
- package/dist/lib/tap.js +9 -0
- package/dist/lib/tier.d.ts +115 -0
- package/dist/lib/tier.js +207 -0
- package/dist/lib/tiers.d.ts +9 -0
- package/dist/lib/tiers.js +141 -0
- package/dist/lib/todo.d.ts +125 -0
- package/dist/lib/todo.js +379 -0
- package/dist/look.d.ts +1 -0
- package/dist/look.js +21 -0
- package/dist/meters.d.ts +8 -0
- package/dist/meters.js +31 -0
- package/dist/model.d.ts +4 -0
- package/dist/model.js +44 -0
- package/dist/network.d.ts +64 -0
- package/dist/network.js +216 -0
- package/dist/open.d.ts +81 -0
- package/dist/open.js +149 -0
- package/dist/pane.d.ts +92 -0
- package/dist/pane.js +231 -0
- package/dist/progress.d.ts +46 -0
- package/dist/progress.js +51 -0
- package/dist/recent.d.ts +67 -0
- package/dist/recent.js +161 -0
- package/dist/rooms.d.ts +131 -0
- package/dist/rooms.js +195 -0
- package/dist/roster.d.ts +28 -0
- package/dist/roster.js +70 -0
- package/dist/span.d.ts +14 -0
- package/dist/span.js +50 -0
- package/dist/speech.d.ts +44 -0
- package/dist/speech.js +229 -0
- package/dist/stars.d.ts +16 -0
- package/dist/stars.js +75 -0
- package/dist/tabs.d.ts +26 -0
- package/dist/tabs.js +25 -0
- package/dist/team.d.ts +11 -0
- package/dist/team.gen.d.ts +10 -0
- package/dist/team.gen.js +11 -0
- package/dist/team.js +30 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +143 -0
- package/dist/tours.d.ts +24 -0
- package/dist/tours.js +311 -0
- package/dist/waiting.d.ts +15 -0
- package/dist/waiting.js +17 -0
- package/dist/where.d.ts +54 -0
- package/dist/where.js +14 -0
- package/package.json +108 -0
package/dist/Meet.d.ts
ADDED
package/dist/Meet.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
4
|
+
import { Video, VideoOff, Mic, MicOff, PhoneOff, Copy, Check, Plus, Calendar, ExternalLink, Bot, Volume2, Square, Send, UserPlus, Tv, LayoutGrid, ArrowRight, } from 'lucide-react';
|
|
5
|
+
import { Text, View, XStack, YStack } from '@hanzo/gui';
|
|
6
|
+
import { Button } from '@hanzo/ui';
|
|
7
|
+
import { Face, voiceOf } from './cast.js';
|
|
8
|
+
import { member } from './team.js';
|
|
9
|
+
import { speakAgent, stopAgentSpeech, AudioWave, useAgentSpeech } from './speech.js';
|
|
10
|
+
import { GOOD } from './lib/mix.js';
|
|
11
|
+
/** A core member as a meeting seats them: the team's name and part, the cast's voice. */
|
|
12
|
+
const seated = (id) => {
|
|
13
|
+
const one = member(id);
|
|
14
|
+
return { id, name: one.name, role: one.role, voice: voiceOf(id) };
|
|
15
|
+
};
|
|
16
|
+
export const AVAILABLE_MEETING_AGENTS = [
|
|
17
|
+
{ id: 'hanzo-coder', name: 'Hanzo Coder', role: 'Engineering Lead & Coder', voice: 'ash' },
|
|
18
|
+
{ id: 'hanzo-researcher', name: 'Hanzo Researcher', role: 'Research & DeSci Scientist', voice: 'fable' },
|
|
19
|
+
seated('maya'),
|
|
20
|
+
seated('des'),
|
|
21
|
+
seated('vi'),
|
|
22
|
+
seated('nora'),
|
|
23
|
+
seated('einstein'),
|
|
24
|
+
{ id: 'zach', name: 'Zach', role: 'Quantitative Analyst', voice: 'onyx' },
|
|
25
|
+
];
|
|
26
|
+
const DEFAULT_MEETINGS = [
|
|
27
|
+
{
|
|
28
|
+
id: 'meet-1',
|
|
29
|
+
title: 'AI Platform Architecture & LLM Inference Sync',
|
|
30
|
+
roomName: 'platform-architecture',
|
|
31
|
+
time: '18:00 - 18:45',
|
|
32
|
+
date: 'Today',
|
|
33
|
+
duration: '45m',
|
|
34
|
+
host: 'Alex (You)',
|
|
35
|
+
participants: ['Alex', 'Zach'],
|
|
36
|
+
agents: ['Hanzo Coder', 'Vi'],
|
|
37
|
+
botInvited: true,
|
|
38
|
+
url: 'https://meet.hanzo.ai/platform-architecture',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'meet-2',
|
|
42
|
+
title: 'Research Review',
|
|
43
|
+
roomName: 'research-review',
|
|
44
|
+
time: '20:00 - 21:00',
|
|
45
|
+
date: 'Today',
|
|
46
|
+
duration: '60m',
|
|
47
|
+
host: 'Research',
|
|
48
|
+
participants: ['Alex', 'Research Team'],
|
|
49
|
+
agents: ['Hanzo Researcher', 'Maya'],
|
|
50
|
+
botInvited: true,
|
|
51
|
+
url: 'https://meet.hanzo.ai/research-review',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'meet-3',
|
|
55
|
+
title: 'Security & Compliance Sync',
|
|
56
|
+
roomName: 'security-sync',
|
|
57
|
+
time: '11:00 - 11:30',
|
|
58
|
+
date: 'Tomorrow',
|
|
59
|
+
duration: '30m',
|
|
60
|
+
host: 'Security',
|
|
61
|
+
participants: ['Alex', 'Core Developers'],
|
|
62
|
+
agents: ['Des', 'Einstein'],
|
|
63
|
+
botInvited: false,
|
|
64
|
+
url: 'https://meet.hanzo.ai/security-sync',
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
async function generateAgentMeetingResponse(prompt, agentName) {
|
|
68
|
+
const token = typeof window !== 'undefined' ? localStorage.getItem('hanzo_iam_access_token') : null;
|
|
69
|
+
if (token) {
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch('https://api.hanzo.ai/v1/chat/completions', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
Authorization: `Bearer ${token}`,
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
model: 'gpt-4o-mini',
|
|
79
|
+
messages: [
|
|
80
|
+
{
|
|
81
|
+
role: 'system',
|
|
82
|
+
content: `You are ${agentName}, participating in a live Hanzo Meet video call. Respond succinctly, conversationally, and informatively in 1 to 2 sentences suitable for voice synthesis.`,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: prompt,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
max_tokens: 150,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
if (res.ok) {
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
const text = data.choices?.[0]?.message?.content?.trim();
|
|
95
|
+
if (text)
|
|
96
|
+
return text;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
}
|
|
101
|
+
// Domain-specific intelligent persona responses
|
|
102
|
+
const lower = prompt.toLowerCase();
|
|
103
|
+
const agent = agentName.toLowerCase();
|
|
104
|
+
if (agent.includes('coder')) {
|
|
105
|
+
if (lower.includes('audio') || lower.includes('webrtc') || lower.includes('media')) {
|
|
106
|
+
return 'The WebRTC audio track is streaming over LiveKit SFU at 24kHz with PCM capture and real-time turn-taking.';
|
|
107
|
+
}
|
|
108
|
+
if (lower.includes('deploy') || lower.includes('build') || lower.includes('status')) {
|
|
109
|
+
return 'All services and container builds are verified green across Kubernetes pods with zero regressions.';
|
|
110
|
+
}
|
|
111
|
+
return `As Hanzo Coder, I am tracking that: our engineering pipeline is synchronized and ready for production deployment.`;
|
|
112
|
+
}
|
|
113
|
+
if (agent.includes('researcher')) {
|
|
114
|
+
return `From our DeSci research benchmarks, model convergence and validation loss show optimal scaling across the cluster.`;
|
|
115
|
+
}
|
|
116
|
+
if (agent.includes('maya')) {
|
|
117
|
+
return `I have recorded this decision and scheduled the follow-up milestone in our calendar.`;
|
|
118
|
+
}
|
|
119
|
+
if (agent.includes('des')) {
|
|
120
|
+
return `The UI hierarchy, contrast ratios, and layout flow are optimized for clarity across both desktop and mobile.`;
|
|
121
|
+
}
|
|
122
|
+
if (agent.includes('vi')) {
|
|
123
|
+
return `Cluster health, P99 response latencies, and auto-scaling ingress controllers are reporting 100% operational.`;
|
|
124
|
+
}
|
|
125
|
+
return `Understood. I have logged that item for our meeting sync and will monitor the execution.`;
|
|
126
|
+
}
|
|
127
|
+
/** design's smallest rung (`--text-floor`, 10px), below gui's `$1`; gui's font size takes a rung or a number. */
|
|
128
|
+
const FLOOR = 10;
|
|
129
|
+
export function Meet() {
|
|
130
|
+
const [activeMeetingRoom, setActiveMeetingRoom] = useState(null);
|
|
131
|
+
const [roomInput, setRoomInput] = useState('');
|
|
132
|
+
const [copied, setCopied] = useState(false);
|
|
133
|
+
const [cameraOn, setCameraOn] = useState(true);
|
|
134
|
+
const [micOn, setMicOn] = useState(true);
|
|
135
|
+
const [botAssisting, setBotAssisting] = useState(true);
|
|
136
|
+
// Below md the co-pilot is a sheet over the stage, opened from the bar.
|
|
137
|
+
const [copilot, setCopilot] = useState(false);
|
|
138
|
+
const [meetings] = useState(DEFAULT_MEETINGS);
|
|
139
|
+
// Agent Invitation and Active Call Participation
|
|
140
|
+
const [invitedAgents, setInvitedAgents] = useState(['Hanzo Coder', 'Hanzo Researcher']);
|
|
141
|
+
const [activeCoPilotAgent, setActiveCoPilotAgent] = useState('Hanzo Coder');
|
|
142
|
+
const [showAgentPicker, setShowAgentPicker] = useState(false);
|
|
143
|
+
const [meetingView, setMeetingView] = useState('iframe');
|
|
144
|
+
const [agentQuestion, setAgentQuestion] = useState('');
|
|
145
|
+
// Agent Speech in Call
|
|
146
|
+
const { speaking, speak, toggle } = useAgentSpeech(activeCoPilotAgent);
|
|
147
|
+
const [audioTesting, setAudioTesting] = useState(false);
|
|
148
|
+
const [handsFree, setHandsFree] = useState(false);
|
|
149
|
+
const recognitionRef = useRef(null);
|
|
150
|
+
const [liveNotes, setLiveNotes] = useState([
|
|
151
|
+
'18:02: Meeting started with participants. AI Bot transcription active.',
|
|
152
|
+
'18:04: Discussed latency metrics on local S3 drive server (port 9005). Average response: 1.2ms.',
|
|
153
|
+
'18:08: Agreed on channel connector architecture: channels connect via bot instances exclusively.',
|
|
154
|
+
'18:12: Action Item assigned to Hanzo Coder: Integrate Cal.com scheduling engine across bot triggers.',
|
|
155
|
+
]);
|
|
156
|
+
const videoRef = useRef(null);
|
|
157
|
+
const localStreamRef = useRef(null);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (activeMeetingRoom) {
|
|
160
|
+
if (localStreamRef.current) {
|
|
161
|
+
localStreamRef.current.getTracks().forEach((t) => t.stop());
|
|
162
|
+
localStreamRef.current = null;
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (cameraOn && typeof navigator !== 'undefined' && navigator.mediaDevices?.getUserMedia) {
|
|
167
|
+
navigator.mediaDevices
|
|
168
|
+
.getUserMedia({ video: true, audio: false })
|
|
169
|
+
.then((stream) => {
|
|
170
|
+
localStreamRef.current = stream;
|
|
171
|
+
if (videoRef.current) {
|
|
172
|
+
videoRef.current.srcObject = stream;
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
.catch(() => { });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
if (localStreamRef.current) {
|
|
179
|
+
localStreamRef.current.getTracks().forEach((t) => t.stop());
|
|
180
|
+
localStreamRef.current = null;
|
|
181
|
+
}
|
|
182
|
+
if (videoRef.current) {
|
|
183
|
+
videoRef.current.srcObject = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return () => {
|
|
187
|
+
if (localStreamRef.current) {
|
|
188
|
+
localStreamRef.current.getTracks().forEach((t) => t.stop());
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}, [cameraOn, activeMeetingRoom]);
|
|
192
|
+
// Stop speech when meeting closes
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
return () => {
|
|
195
|
+
stopAgentSpeech();
|
|
196
|
+
};
|
|
197
|
+
}, []);
|
|
198
|
+
const startInstantMeeting = () => {
|
|
199
|
+
const randomId = `hanzo-${Math.random().toString(36).slice(2, 8)}`;
|
|
200
|
+
setActiveMeetingRoom(randomId);
|
|
201
|
+
};
|
|
202
|
+
const joinCustomMeeting = () => {
|
|
203
|
+
if (!roomInput.trim())
|
|
204
|
+
return;
|
|
205
|
+
let clean = roomInput.trim();
|
|
206
|
+
if (clean.includes('meet.hanzo.ai/')) {
|
|
207
|
+
clean = clean.split('meet.hanzo.ai/')[1].split('?')[0].split('#')[0];
|
|
208
|
+
}
|
|
209
|
+
setActiveMeetingRoom(clean);
|
|
210
|
+
};
|
|
211
|
+
const handleCopyLink = (room) => {
|
|
212
|
+
const link = `https://meet.hanzo.ai/${room}`;
|
|
213
|
+
navigator.clipboard.writeText(link);
|
|
214
|
+
setCopied(true);
|
|
215
|
+
setTimeout(() => setCopied(false), 2000);
|
|
216
|
+
};
|
|
217
|
+
const toggleAgentInvite = (agentName) => {
|
|
218
|
+
setInvitedAgents((prev) => {
|
|
219
|
+
if (prev.includes(agentName)) {
|
|
220
|
+
const next = prev.filter((a) => a !== agentName);
|
|
221
|
+
return next.length > 0 ? next : [agentName];
|
|
222
|
+
}
|
|
223
|
+
return [...prev, agentName];
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
const testAudioSpeaker = () => {
|
|
227
|
+
if (audioTesting) {
|
|
228
|
+
stopAgentSpeech();
|
|
229
|
+
setAudioTesting(false);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
setAudioTesting(true);
|
|
233
|
+
speakAgent(`Welcome to Hanzo Meet. Speaker test complete. All agent audio and video systems are ready for this call.`, {
|
|
234
|
+
agent: activeCoPilotAgent,
|
|
235
|
+
onEnd: () => setAudioTesting(false),
|
|
236
|
+
onError: () => setAudioTesting(false),
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
const handleSpokenInput = useCallback(async (transcript) => {
|
|
240
|
+
const time = new Date().toLocaleTimeString().slice(0, 5);
|
|
241
|
+
const userNote = `${time}: You (voice): ${transcript}`;
|
|
242
|
+
setLiveNotes((prev) => [...prev, userNote]);
|
|
243
|
+
const reply = await generateAgentMeetingResponse(transcript, activeCoPilotAgent);
|
|
244
|
+
const agentNote = `${time}: ${activeCoPilotAgent} (AI): ${reply}`;
|
|
245
|
+
setLiveNotes((prev) => [...prev, agentNote]);
|
|
246
|
+
speak(reply, activeCoPilotAgent);
|
|
247
|
+
}, [activeCoPilotAgent, speak]);
|
|
248
|
+
// Real-time hands-free speech recognition & barge-in turn-taking
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (!handsFree || !activeMeetingRoom) {
|
|
251
|
+
if (recognitionRef.current) {
|
|
252
|
+
try {
|
|
253
|
+
recognitionRef.current.stop();
|
|
254
|
+
}
|
|
255
|
+
catch { }
|
|
256
|
+
recognitionRef.current = null;
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (typeof window === 'undefined')
|
|
261
|
+
return;
|
|
262
|
+
const SpeechRec = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
263
|
+
if (!SpeechRec)
|
|
264
|
+
return;
|
|
265
|
+
const recognition = new SpeechRec();
|
|
266
|
+
recognition.continuous = true;
|
|
267
|
+
recognition.interimResults = true;
|
|
268
|
+
recognition.lang = 'en-US';
|
|
269
|
+
recognition.onresult = (event) => {
|
|
270
|
+
// Barge-in: if human starts speaking while agent is talking, halt speech immediately!
|
|
271
|
+
if (speaking) {
|
|
272
|
+
stopAgentSpeech();
|
|
273
|
+
}
|
|
274
|
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
|
275
|
+
if (event.results[i].isFinal) {
|
|
276
|
+
const transcript = event.results[i][0]?.transcript?.trim();
|
|
277
|
+
if (transcript) {
|
|
278
|
+
handleSpokenInput(transcript);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
recognition.onerror = () => { };
|
|
284
|
+
recognition.onend = () => {
|
|
285
|
+
if (handsFree && activeMeetingRoom) {
|
|
286
|
+
try {
|
|
287
|
+
recognition.start();
|
|
288
|
+
}
|
|
289
|
+
catch { }
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
try {
|
|
293
|
+
recognition.start();
|
|
294
|
+
recognitionRef.current = recognition;
|
|
295
|
+
}
|
|
296
|
+
catch { }
|
|
297
|
+
return () => {
|
|
298
|
+
if (recognitionRef.current) {
|
|
299
|
+
try {
|
|
300
|
+
recognitionRef.current.stop();
|
|
301
|
+
}
|
|
302
|
+
catch { }
|
|
303
|
+
recognitionRef.current = null;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
}, [handsFree, activeMeetingRoom, speaking, handleSpokenInput]);
|
|
307
|
+
const handleAskAgent = async (e) => {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
const q = agentQuestion.trim();
|
|
310
|
+
if (!q)
|
|
311
|
+
return;
|
|
312
|
+
setAgentQuestion('');
|
|
313
|
+
const time = new Date().toLocaleTimeString().slice(0, 5);
|
|
314
|
+
const userNote = `${time}: You: ${q}`;
|
|
315
|
+
setLiveNotes((prev) => [...prev, userNote]);
|
|
316
|
+
const reply = await generateAgentMeetingResponse(q, activeCoPilotAgent);
|
|
317
|
+
const agentNote = `${time}: ${activeCoPilotAgent} (AI): ${reply}`;
|
|
318
|
+
setLiveNotes((prev) => [...prev, agentNote]);
|
|
319
|
+
speak(reply, activeCoPilotAgent);
|
|
320
|
+
};
|
|
321
|
+
const handleReadBriefing = () => {
|
|
322
|
+
const summary = `Executive briefing for ${activeMeetingRoom || 'call'}. Key items discussed: latency metrics on local S3 drive server average 1.2ms, channel connector architecture verified, Cal scheduling integration underway.`;
|
|
323
|
+
toggle(summary, activeCoPilotAgent);
|
|
324
|
+
};
|
|
325
|
+
return (_jsxs(YStack, { width: "100%", height: "100%", bg: "$background", overflowX: "auto", overflowY: "auto", children: [_jsxs(XStack, { minH: 56, px: 20, py: 8, gap: 10, flexWrap: "wrap", bg: "$panel", borderBottomWidth: 1, borderBottomColor: "$borderColor", items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: 12, shrink: 1, minW: "auto", children: [_jsx(XStack, { width: 32, height: 32, rounded: 8, bg: "$raised", items: "center", justify: "center", shrink: 1, minW: "auto", children: _jsx(Video, { size: 18, color: "var(--foreground)" }) }), _jsxs(YStack, { shrink: 1, minW: "auto", children: [_jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Text, { fontSize: "$4", fontWeight: "700", color: "$ink", children: "Hanzo Meet" }), _jsx(Text, { fontSize: FLOOR, fontWeight: "700", bg: "$hover", color: "$soft", borderWidth: 1, borderColor: "$borderColor", px: 6, py: 1, rounded: 4, children: "LIVE \u00B7 meet.hanzo.ai" })] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Native WebRTC video conferencing with AI meeting bot transcription & voice" })] })] }), activeMeetingRoom ? (_jsxs(XStack, { items: "center", gap: 8, shrink: 1, minW: "auto", flexWrap: "wrap", children: [_jsx(XStack, { bg: "$hover", rounded: 8, p: 2, gap: 2, borderWidth: 1, borderColor: "$borderColor", shrink: 1, minW: "auto", children: [
|
|
326
|
+
['iframe', 'SFU Stage', Tv],
|
|
327
|
+
['grid', 'Agent Grid', LayoutGrid],
|
|
328
|
+
].map(([view, label, Icon]) => (_jsxs(Button, { size: "sm", variant: meetingView === view ? 'primary' : 'ghost', ...(meetingView === view ? null : { color: '$soft' }), "aria-pressed": meetingView === view, onClick: () => setMeetingView(view), children: [_jsx(Icon, { size: 12 }), label] }, view))) }), botAssisting ? (_jsx(View, { display: "none", "$max-md": { display: 'flex' }, children: _jsxs(Button, { size: "sm", variant: copilot ? 'secondary' : 'default', onClick: () => setCopilot((v) => !v), "aria-label": "Co-pilot", "aria-expanded": copilot, "aria-controls": "meet-copilot", children: [_jsx(Bot, { size: 13 }), "Co-pilot"] }) })) : null, _jsxs(Button, { size: "sm", onClick: () => handleCopyLink(activeMeetingRoom), children: [copied ? _jsx(Check, { size: 13 }) : _jsx(Copy, { size: 13 }), copied ? 'Link Copied' : 'Copy Room Link'] }), _jsxs(Button, { size: "sm", variant: "destructive", onClick: () => {
|
|
329
|
+
stopAgentSpeech();
|
|
330
|
+
setActiveMeetingRoom(null);
|
|
331
|
+
}, children: [_jsx(PhoneOff, { size: 14 }), "Leave Call"] })] })) : (_jsx(XStack, { items: "center", gap: 8, shrink: 1, minW: "auto", children: _jsx(Button, { asChild: true, variant: "linkMuted", size: "sm", px: 0, children: _jsxs("a", { href: "https://meet.hanzo.ai", target: "_blank", rel: "noopener noreferrer", children: ["meet.hanzo.ai", _jsx(ExternalLink, { size: 12 })] }) }) }))] }), activeMeetingRoom ? (_jsxs(XStack, { flex: 1, overflow: "hidden", position: "relative", children: [_jsx(XStack, { flex: 1, minW: "auto", position: "relative", bg: "$sunken", children: meetingView === 'iframe' ? (_jsx(View, { render: _jsx("iframe", { src: `https://meet.hanzo.ai/${activeMeetingRoom}#config.prejoinPageEnabled=false&config.startWithAudioMuted=${!micOn}&config.startWithVideoMuted=${!cameraOn}`, allow: "camera; microphone; display-capture; autoplay; clipboard-write; speaker-selection; screen-wake-lock", title: `Hanzo Meet Room: ${activeMeetingRoom}` }), width: "100%", height: "100%", borderWidth: 0, bg: "$sunken" }, "stage")) : (_jsxs(View, { flex: 1, display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: 12, p: 16, overflowY: "auto", bg: "$background", children: [_jsx(YStack, { bg: "$panel", rounded: 12, borderWidth: 1, borderColor: "$borderColor", position: "relative", overflow: "hidden", items: "center", justify: "center", minH: 220, children: _jsxs(YStack, { items: "center", children: [_jsx(YStack, { width: 72, height: 72, rounded: 9999, bg: "$raised", items: "center", justify: "center", mb: 8, children: _jsx(Text, { fontSize: "$8", fontWeight: "700", color: "$ink", children: "You" }) }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: "Alex (Host)" }), _jsx(Text, { fontSize: "$1", color: micOn ? GOOD : '$soft', children: micOn ? 'Mic Active' : 'Muted' })] }) }), invitedAgents.map((agentName) => {
|
|
332
|
+
const voice = voiceOf(agentName);
|
|
333
|
+
const isThisSpeaking = speaking && activeCoPilotAgent.toLowerCase() === agentName.toLowerCase();
|
|
334
|
+
return (_jsxs(YStack, { bg: "$panel", rounded: 12, borderWidth: isThisSpeaking ? 2 : 1, borderColor: isThisSpeaking ? 'var(--state-success)' : '$borderColor', position: "relative", overflow: "hidden", items: "center", justify: "center", p: 20, minH: 220, transition: "200ms", children: [_jsxs(View, { position: "relative", mb: 10, children: [_jsx(Face, { name: agentName, size: 72 }), isThisSpeaking ? (_jsx(View, { position: "absolute", b: -4, r: -4, bg: "var(--popover)", borderWidth: 1, borderColor: "var(--state-success)", rounded: 9999, p: 4, children: _jsx(Volume2, { size: 12, color: GOOD }) })) : null] }), _jsx(Text, { fontSize: "$3", fontWeight: "700", color: "$ink", mb: 2, children: agentName }), _jsxs(Text, { fontSize: "$1", color: "$soft", mb: 8, children: ["Voice: ", voice] }), _jsx(XStack, { items: "center", gap: 6, bg: "$hover", px: 10, py: 4, rounded: 12, children: isThisSpeaking ? (_jsxs(_Fragment, { children: [_jsx(AudioWave, { active: true, color: "var(--state-success)" }), _jsx(Text, { fontSize: "$1", fontWeight: "600", color: GOOD, children: "Speaking in Call" })] })) : (_jsxs(_Fragment, { children: [_jsx(Bot, { size: 11, color: "var(--muted-foreground)" }), _jsx(Text, { fontSize: "$1", fontWeight: "600", color: "$quiet", children: "AI Agent \u00B7 In Call" })] })) })] }, agentName));
|
|
335
|
+
})] }, "grid")) }), botAssisting ? (_jsxs(YStack, { id: "meet-copilot", width: 360, minW: 360, bg: "$panel", borderLeftWidth: 1, borderLeftColor: "$borderColor", p: 16, gap: 12, "$max-md": {
|
|
336
|
+
display: copilot ? 'flex' : 'none',
|
|
337
|
+
position: 'absolute',
|
|
338
|
+
t: 0,
|
|
339
|
+
r: 0,
|
|
340
|
+
b: 0,
|
|
341
|
+
l: 0,
|
|
342
|
+
width: 'auto',
|
|
343
|
+
minW: 0,
|
|
344
|
+
z: 'var(--z-sheet)',
|
|
345
|
+
borderLeftWidth: 0,
|
|
346
|
+
}, children: [_jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Face, { name: activeCoPilotAgent, size: 28 }), _jsxs(YStack, { children: [_jsx(Text, { fontSize: "$2", fontWeight: "700", color: "$ink", children: activeCoPilotAgent }), _jsxs(Text, { fontSize: FLOOR, color: "$faint", children: ["AI Co-pilot \u00B7 Voice: ", voiceOf(activeCoPilotAgent)] })] })] }), _jsx(Text, { fontSize: FLOOR, fontWeight: "700", color: GOOD, bg: "$hover", px: 6, py: 2, rounded: 4, children: "Live in call" })] }), _jsx(XStack, { gap: 4, flexWrap: "wrap", children: invitedAgents.map((agent) => (_jsx(Button, { size: "sm", variant: activeCoPilotAgent === agent ? 'secondary' : 'ghost', color: activeCoPilotAgent === agent ? '$ink' : '$soft', "aria-pressed": activeCoPilotAgent === agent, onClick: () => setActiveCoPilotAgent(agent), children: agent }, agent))) }), _jsxs(XStack, { gap: 8, children: [_jsx(Button, { size: "sm", flex: 1, variant: speaking ? 'secondary' : 'default', onClick: handleReadBriefing, children: speaking ? (_jsxs(_Fragment, { children: [_jsx(AudioWave, { active: true, color: "var(--state-success)" }), "Stop Briefing", _jsx(Square, { size: 11, fill: "currentColor" })] })) : (_jsxs(_Fragment, { children: [_jsx(Volume2, { size: 14 }), "Briefing"] })) }), _jsxs(Button, { size: "sm", variant: handsFree ? 'secondary' : 'default', "data-testid": "hands-free-voice-toggle", "aria-pressed": handsFree, title: handsFree ? 'Hands-Free Voice Active (Speaks and interrupts on voice)' : 'Enable Hands-Free Voice Mode', onClick: () => setHandsFree(!handsFree), children: [_jsx(Mic, { size: 14, color: handsFree ? 'var(--state-success)' : 'currentColor' }), handsFree ? 'Hands-Free On' : 'Hands-Free'] })] }), _jsx(YStack, { flex: 1, overflowY: "auto", bg: "$background", rounded: 8, borderWidth: 1, borderColor: "$borderColor", p: 12, gap: 8, children: liveNotes.map((note, i) => (_jsx(Text, { fontSize: "$1", color: "$quiet", lineHeight: 16.5, pb: 8, borderBottomWidth: 1, borderBottomColor: "$borderColor", children: note }, i))) }), _jsxs(XStack, { render: "form", onSubmit: handleAskAgent, gap: 6, children: [_jsx(Text, { render: _jsx("input", { type: "text", value: agentQuestion, onChange: (e) => setAgentQuestion(e.target.value), placeholder: `Ask ${activeCoPilotAgent} in meeting...` }), flex: 1, bg: "$background", borderWidth: 1, borderColor: "var(--border-control)", rounded: 6, px: 10, py: 8, color: "$ink", fontSize: "$1", outlineStyle: "none" }), _jsx(Button, { type: "submit", variant: "primary", size: "icon-sm", disabled: !agentQuestion.trim(), "aria-label": "Ask", children: _jsx(Send, { size: 12 }) })] }), _jsxs(XStack, { gap: 8, children: [_jsx(Button, { size: "sm", flex: 1, onClick: () => {
|
|
347
|
+
const sample = `${new Date().toLocaleTimeString().slice(0, 5)}: Participant discussed deployment timeline for next release.`;
|
|
348
|
+
setLiveNotes((prev) => [...prev, sample]);
|
|
349
|
+
}, children: "+ Add Live Note" }), _jsx(Button, { variant: "primary", size: "sm", onClick: () => {
|
|
350
|
+
const text = liveNotes.join('\n');
|
|
351
|
+
navigator.clipboard.writeText(text);
|
|
352
|
+
}, children: "Copy Summary" })] })] })) : null] })) : (_jsxs(XStack, { flex: 1, overflow: "hidden", "$max-lg": { flexDirection: 'column', overflowY: 'auto', overflowX: 'hidden' }, children: [_jsxs(YStack, { flex: 1, minW: 0, p: 32, gap: 24, overflowY: "auto", "$max-lg": { p: 16, flexBasis: 'auto', shrink: 0, overflowY: 'visible', overflowX: 'hidden' }, children: [_jsxs(YStack, { gap: 6, children: [_jsx(Text, { fontSize: "$8", fontWeight: "800", color: "$ink", letterSpacing: -0.5, children: "Secure, High-Definition Video Calls" }), _jsxs(Text, { fontSize: "$3", color: "$soft", children: ["Powered by native Hanzo Meet (", _jsx(Text, { render: _jsx("a", { href: "https://meet.hanzo.ai", target: "_blank", rel: "noopener noreferrer" }), color: "$ink", textDecorationLine: "underline", children: "meet.hanzo.ai" }), "). Connect with team members, clients, and autonomous AI agents."] })] }), _jsxs(XStack, { gap: 12, flexWrap: "wrap", children: [_jsxs(Button, { variant: "primary", size: "lg", onClick: startInstantMeeting, children: [_jsx(Plus, { size: 16 }), "Start Instant Meeting"] }), _jsxs(XStack, { items: "center", gap: 6, bg: "$panel", borderWidth: 1, borderColor: "var(--border-control)", rounded: 8, px: 4, height: 42, children: [_jsx(Text, { render: _jsx("input", { type: "text", value: roomInput, onChange: (e) => setRoomInput(e.target.value), onKeyDown: (e) => e.key === 'Enter' && joinCustomMeeting(), placeholder: "Enter meeting code or link", "aria-label": "Meeting code or link" }), bg: "transparent", borderWidth: 0, outlineStyle: "none", color: "$ink", fontSize: "$2", width: 220, px: 8 }), _jsx(Button, { variant: "primary", size: "sm", disabled: !roomInput.trim(), onClick: joinCustomMeeting, children: "Join" })] }), _jsxs(Button, { size: "lg", variant: showAgentPicker ? 'secondary' : 'default', "aria-expanded": showAgentPicker, onClick: () => setShowAgentPicker(!showAgentPicker), children: [_jsx(UserPlus, { size: 14 }), `Invite Agents (${invitedAgents.length})`] })] }), showAgentPicker ? (_jsxs(YStack, { bg: "$panel", borderWidth: 1, borderColor: "var(--border-control)", rounded: 12, p: 16, maxW: 560, children: [_jsxs(XStack, { items: "center", justify: "space-between", mb: 12, children: [_jsx(Text, { fontSize: "$2", fontWeight: "700", color: "$ink", children: "Select AI Agents to Attend Meetings" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Each agent joins the call with voice synthesis & pair programming" })] }), _jsx(View, { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 8, children: AVAILABLE_MEETING_AGENTS.map((agent) => {
|
|
353
|
+
const isInvited = invitedAgents.includes(agent.name);
|
|
354
|
+
return (_jsxs(XStack, { onClick: () => toggleAgentInvite(agent.name), items: "center", gap: 10, px: 10, py: 8, rounded: 8, bg: isInvited ? '$raised' : '$hover', borderWidth: 1, borderColor: isInvited ? 'var(--border-selected)' : '$borderColor', cursor: "pointer", transition: "quickest", children: [_jsx(Face, { name: agent.name, size: 32 }), _jsxs(YStack, { flex: 1, minW: 0, children: [_jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", children: agent.name }), _jsxs(Text, { fontSize: 10, color: "$faint", numberOfLines: 1, children: [agent.role, " \u00B7 Voice: ", agent.voice] })] }), _jsx(View, { width: 16, height: 16, rounded: 4, borderWidth: isInvited ? 0 : 1, borderColor: "var(--border-selected)", bg: isInvited ? 'var(--foreground)' : 'transparent', items: "center", justify: "center", children: isInvited ? _jsx(Check, { size: 11, strokeWidth: 3, color: "var(--background)" }) : null })] }, agent.id));
|
|
355
|
+
}) })] })) : null, _jsxs(YStack, { bg: "$panel", borderWidth: 1, borderColor: "$borderColor", rounded: 12, overflow: "hidden", maxW: 560, children: [_jsxs(XStack, { height: 280, bg: "$sunken", position: "relative", items: "center", justify: "center", children: [cameraOn ? (_jsx(View, { render: _jsx("video", { ref: videoRef, autoPlay: true, playsInline: true, muted: true }), width: "100%", height: "100%", objectFit: "cover" })) : (_jsxs(YStack, { items: "center", gap: 8, children: [_jsx(XStack, { width: 64, height: 64, rounded: 9999, bg: "$raised", items: "center", justify: "center", children: _jsx(VideoOff, { size: 28, color: "var(--muted-foreground)" }) }), _jsx(Text, { fontSize: "$2", color: "$soft", children: "Camera is off" })] })), _jsxs(XStack, { position: "absolute", b: 16, maxW: "calc(100% - 24px)", items: "center", justify: "center", flexWrap: "wrap", gap: 8, bg: "var(--surface-overlay)", backdropFilter: "blur(8px)", px: 12, py: 6, rounded: 24, borderWidth: 1, borderColor: "$borderColor", children: [_jsx(Button, { size: "icon", rounded: 9999, variant: cameraOn ? 'default' : 'destructive', "aria-label": cameraOn ? 'Turn Camera Off' : 'Turn Camera On', title: cameraOn ? 'Turn Camera Off' : 'Turn Camera On', onClick: () => setCameraOn(!cameraOn), children: cameraOn ? _jsx(Video, { size: 16 }) : _jsx(VideoOff, { size: 16 }) }), _jsx(Button, { size: "icon", rounded: 9999, variant: micOn ? 'default' : 'destructive', "aria-label": micOn ? 'Mute Mic' : 'Unmute Mic', title: micOn ? 'Mute Mic' : 'Unmute Mic', onClick: () => setMicOn(!micOn), children: micOn ? _jsx(Mic, { size: 16 }) : _jsx(MicOff, { size: 16 }) }), _jsx(Button, { size: "sm", rounded: 9999, variant: audioTesting ? 'secondary' : 'default', title: "Test Agent Voice & Audio Output", onClick: testAudioSpeaker, children: audioTesting ? (_jsxs(_Fragment, { children: [_jsx(AudioWave, { active: true, color: "var(--state-success)" }), "Testing Audio\u2026"] })) : (_jsxs(_Fragment, { children: [_jsx(Volume2, { size: 13 }), "Test Audio"] })) }), _jsxs(Button, { size: "sm", rounded: 9999, variant: botAssisting ? 'secondary' : 'default', "aria-pressed": botAssisting, onClick: () => setBotAssisting(!botAssisting), children: [_jsx(Bot, { size: 13 }), botAssisting ? 'AI Bot Co-pilot ON' : 'AI Bot OFF'] })] })] }), _jsxs(XStack, { p: 12, items: "center", justify: "space-between", bg: "$panel", children: [_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Readiness: Camera ", cameraOn ? 'Active' : 'Muted', " \u00B7 Mic ", micOn ? 'Active' : 'Muted', " \u00B7 Audio ", audioTesting ? 'Testing' : 'Ready'] }), _jsx(Text, { fontSize: "$1", color: "$quiet", fontWeight: "600", children: "HD 1080p Ready" })] })] })] }), _jsxs(YStack, { grow: 1, shrink: 1, flexBasis: 380, minW: 0, maxW: "100%", bg: "$panel", borderLeftWidth: 1, borderLeftColor: "$borderColor", p: 16, gap: 14, overflowY: "auto", "$max-lg": {
|
|
356
|
+
shrink: 0,
|
|
357
|
+
flexBasis: 'auto',
|
|
358
|
+
borderLeftWidth: 0,
|
|
359
|
+
borderTopWidth: 1,
|
|
360
|
+
borderTopColor: '$borderColor',
|
|
361
|
+
overflowX: 'hidden',
|
|
362
|
+
}, children: [_jsxs(XStack, { items: "center", justify: "space-between", children: [_jsx(Text, { fontSize: "$3", fontWeight: "700", color: "$ink", children: "Upcoming Meetings" }), _jsx(Text, { fontSize: "$1", color: "$faint", children: "Synced with /cal" })] }), _jsx(YStack, { gap: 10, children: meetings.map((m) => (_jsxs(YStack, { bg: "$background", borderWidth: 1, borderColor: "$borderColor", rounded: 8, p: 12, gap: 8, children: [_jsxs(XStack, { items: "center", justify: "space-between", children: [_jsxs(Text, { fontSize: FLOOR, fontWeight: "700", bg: "$raised", color: "$ink", px: 6, py: 2, rounded: 4, children: [m.date, " \u00B7 ", m.time] }), m.botInvited ? (_jsxs(XStack, { items: "center", gap: 4, children: [_jsx(Bot, { size: 12, color: "var(--muted-foreground)" }), _jsx(Text, { fontSize: FLOOR, color: "$soft", fontWeight: "600", children: "Agents Attending" })] })) : null] }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", lineHeight: 16.9, children: m.title }), _jsxs(XStack, { items: "center", gap: 6, flexWrap: "wrap", children: [_jsxs(Text, { fontSize: "$1", color: "$soft", children: ["Host: ", m.host] }), m.agents && m.agents.length > 0 ? (_jsxs(XStack, { display: "inline-flex", items: "center", gap: 4, children: [_jsx(Text, { fontSize: "$1", color: "$faint", children: "\u00B7" }), _jsx(Text, { fontSize: "$1", color: "$soft", children: m.agents.join(', ') })] })) : null] }), _jsxs(XStack, { gap: 8, mt: 4, children: [_jsxs(Button, { variant: "primary", size: "sm", flex: 1, onClick: () => {
|
|
363
|
+
if (m.agents) {
|
|
364
|
+
setInvitedAgents(m.agents);
|
|
365
|
+
if (m.agents[0])
|
|
366
|
+
setActiveCoPilotAgent(m.agents[0]);
|
|
367
|
+
}
|
|
368
|
+
setActiveMeetingRoom(m.roomName);
|
|
369
|
+
}, children: [_jsx(Video, { size: 12 }), "Join Call"] }), _jsx(Button, { size: "icon-sm", "aria-label": "Copy invite link", title: "Copy invite link", onClick: () => handleCopyLink(m.roomName), children: _jsx(Copy, { size: 12 }) })] })] }, m.id))) }), _jsxs(YStack, { mt: "auto", p: 12, bg: "$background", rounded: 8, borderWidth: 1, borderColor: "$borderColor", gap: 6, children: [_jsxs(XStack, { items: "center", gap: 8, children: [_jsx(Calendar, { size: 14, color: "var(--muted-foreground)" }), _jsx(Text, { fontSize: "$2", fontWeight: "700", color: "$ink", children: "Need to book a slot?" })] }), _jsx(Text, { fontSize: "$1", color: "$soft", children: "Use the Hanzo Calendar Scheduler to coordinate available times across team members and bots." }), _jsx(Button, { asChild: true, variant: "link", size: "sm", px: 0, justify: "flex-start", children: _jsxs("a", { href: "/cal", children: ["Open Calendar Scheduler", _jsx(ArrowRight, { size: 11 })] }) })] })] })] }))] }));
|
|
370
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The product's name in each mode: Hanzo AI in Chat, Hanzo Dev in Dev. */
|
|
2
|
+
export declare const wordmark: (mode: 'chat' | 'dev') => string;
|
|
3
|
+
export declare function ModeMark({ mode, onChat, onDev, stacked, }: {
|
|
4
|
+
mode: 'chat' | 'dev';
|
|
5
|
+
onChat: () => void;
|
|
6
|
+
onDev: () => void;
|
|
7
|
+
stacked?: boolean;
|
|
8
|
+
}): import("react").JSX.Element;
|
package/dist/ModeMark.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { MessageSquare, Code2 } from 'lucide-react';
|
|
4
|
+
import { XStack, YStack, Text, Tooltip, TooltipContent, TooltipTrigger } from '@hanzo/ui';
|
|
5
|
+
/** The product's name in each mode: Hanzo AI in Chat, Hanzo Dev in Dev. */
|
|
6
|
+
export const wordmark = (mode) => (mode === 'dev' ? 'Hanzo Dev' : 'Hanzo AI');
|
|
7
|
+
/** One side of the switch: an icon button named for its mode, with the name as its tooltip. */
|
|
8
|
+
function Side({ name, on, onPress, children }) {
|
|
9
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { children: _jsx(XStack, { render: "button", "aria-label": name, "aria-pressed": on, onPress: onPress, width: 28, height: 24, rounded: 999, items: "center", justify: "center", bg: on ? '$hover' : 'transparent', hoverStyle: { bg: '$hover' }, cursor: "pointer", children: children }) }), _jsx(TooltipContent, { children: _jsx(Text, { fontSize: "$2", children: name }) })] }));
|
|
10
|
+
}
|
|
11
|
+
export function ModeMark({ mode, onChat, onDev, stacked = false, }) {
|
|
12
|
+
const dev = mode === 'dev';
|
|
13
|
+
const Group = stacked ? YStack : XStack;
|
|
14
|
+
const toggle = (_jsxs(Group, { role: "group", "aria-label": "Switch Hanzo", items: "center", gap: 2, p: 2, rounded: 999, bg: "$edge", shrink: 0, children: [_jsx(Side, { name: "Chat", on: !dev, onPress: onChat, children: _jsx(MessageSquare, { size: 14, "aria-hidden": true }) }), _jsx(Side, { name: "Dev", on: dev, onPress: onDev, children: _jsx(Code2, { size: 14, "aria-hidden": true }) })] }));
|
|
15
|
+
if (stacked)
|
|
16
|
+
return toggle;
|
|
17
|
+
return (_jsxs(XStack, { flex: 1, items: "center", justify: "space-between", gap: "$2", minW: 0, children: [_jsx(Text, { fontSize: "$4", fontWeight: "600", color: "$ink", numberOfLines: 1, children: wordmark(mode) }), toggle] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type GuiElement } from '@hanzo/gui';
|
|
2
|
+
export interface OrganizationItem {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
displayName: string;
|
|
6
|
+
logo?: string;
|
|
7
|
+
avatar?: string;
|
|
8
|
+
role?: string;
|
|
9
|
+
isPersonal?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function OrgSwitcherDropdown({ open, onClose, activeOrgId, onSelect, align, side, triggerRef, }: {
|
|
12
|
+
open: boolean;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
activeOrgId: string;
|
|
15
|
+
onSelect: (orgId: string) => void;
|
|
16
|
+
align?: 'left' | 'right';
|
|
17
|
+
/** Which way it opens: below the trigger, above it, or to the right (rail). */
|
|
18
|
+
side?: 'below' | 'above' | 'rail';
|
|
19
|
+
triggerRef?: React.RefObject<GuiElement | null>;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
/**
|
|
22
|
+
* THE WORKSPACE YOU ARE STANDING IN, and the way to another one.
|
|
23
|
+
*
|
|
24
|
+
* IT NAMES THE ORG OR IT NAMES NOTHING. The label fell through to the literal
|
|
25
|
+
* "Hanzo AI" — so a reader in no org, or one whose IAM read had not landed, was
|
|
26
|
+
* told they were inside a company they may have nothing to do with, and the
|
|
27
|
+
* corner that exists to say WHERE YOU ARE was the one part of the app most
|
|
28
|
+
* confidently wrong. `selectedLocal` did the same for the id: `'hanzo'` before
|
|
29
|
+
* IAM answered, which is also the id a press would have switched to.
|
|
30
|
+
*/
|
|
31
|
+
export declare function TopOrgSelector(): import("react").JSX.Element;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useRef, useEffect, useMemo } from 'react';
|
|
4
|
+
import { Check, ChevronDown, Plus, Settings } from 'lucide-react';
|
|
5
|
+
import { HanzoMark } from '@hanzogui/shell';
|
|
6
|
+
import { useOrganizations } from '@hanzo/iam/react';
|
|
7
|
+
import { Button } from '@hanzo/ui';
|
|
8
|
+
import { useAccount } from './lib/account.js';
|
|
9
|
+
import { iam } from './lib/api.js';
|
|
10
|
+
import { pick } from './lib/session.js';
|
|
11
|
+
import { Text, View, XStack, YStack } from '@hanzo/gui';
|
|
12
|
+
import { site } from './where.js';
|
|
13
|
+
/** design's smallest rung (`--text-floor`, 10px), below gui's `$1`; gui's font size takes a rung or a number. */
|
|
14
|
+
const FLOOR = 10;
|
|
15
|
+
/** A row at the foot of the menu that leaves for another page: a ghost Button, label to the left. */
|
|
16
|
+
const LINK = { variant: 'ghost', size: 'sm', width: '100%', justify: 'flex-start' };
|
|
17
|
+
export function OrgSwitcherDropdown({ open, onClose, activeOrgId, onSelect, align = 'left', side = 'below', triggerRef, }) {
|
|
18
|
+
const { organizations: iamOrgs, switchOrg } = useOrganizations() || {};
|
|
19
|
+
const { organizations: accountOrgs, switchOrganization } = useAccount() || {};
|
|
20
|
+
const menuRef = useRef(null);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!open)
|
|
23
|
+
return;
|
|
24
|
+
const handleClickOutside = (e) => {
|
|
25
|
+
const trigger = triggerRef?.current;
|
|
26
|
+
const menu = menuRef.current;
|
|
27
|
+
if (trigger && trigger.contains(e.target)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (menu && !menu.contains(e.target)) {
|
|
31
|
+
onClose();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const handleKeyDown = (e) => {
|
|
35
|
+
if (e.key === 'Escape')
|
|
36
|
+
onClose();
|
|
37
|
+
};
|
|
38
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
39
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
40
|
+
return () => {
|
|
41
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
42
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
43
|
+
};
|
|
44
|
+
}, [open, onClose, triggerRef]);
|
|
45
|
+
const orgs = useMemo(() => {
|
|
46
|
+
const map = new Map();
|
|
47
|
+
// Add detected account orgs
|
|
48
|
+
for (const o of (accountOrgs ?? [])) {
|
|
49
|
+
const item = o;
|
|
50
|
+
const id = item.id || item.name;
|
|
51
|
+
if (id) {
|
|
52
|
+
map.set(id, {
|
|
53
|
+
id,
|
|
54
|
+
name: item.name || id,
|
|
55
|
+
displayName: item.displayName || item.name || id,
|
|
56
|
+
logo: item.logo || item.avatar,
|
|
57
|
+
role: item.role || 'member',
|
|
58
|
+
isPersonal: Boolean(item.isPersonal),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Add detected IAM orgs
|
|
63
|
+
for (const o of (iamOrgs ?? [])) {
|
|
64
|
+
const item = o;
|
|
65
|
+
const id = item.id || item.name;
|
|
66
|
+
if (id && !map.has(id)) {
|
|
67
|
+
map.set(id, {
|
|
68
|
+
id,
|
|
69
|
+
name: item.name || id,
|
|
70
|
+
displayName: item.displayName || item.name || id,
|
|
71
|
+
logo: item.logo || item.avatar,
|
|
72
|
+
role: item.role || 'member',
|
|
73
|
+
isPersonal: Boolean(item.isPersonal),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return Array.from(map.values());
|
|
78
|
+
}, [accountOrgs, iamOrgs]);
|
|
79
|
+
if (!open)
|
|
80
|
+
return null;
|
|
81
|
+
// Where it opens. The offset rides a margin, so each edge is a plain length gui can take.
|
|
82
|
+
const position = side === 'rail'
|
|
83
|
+
? { l: '100%', ml: 10, t: -4 }
|
|
84
|
+
: side === 'above'
|
|
85
|
+
? { b: '100%', mb: 6, ...(align === 'left' ? { l: 0 } : { r: 0 }) }
|
|
86
|
+
: { t: '100%', mt: 6, ...(align === 'left' ? { l: 0 } : { r: 0 }) };
|
|
87
|
+
return (_jsxs(YStack, { ref: menuRef, position: "absolute", ...position, width: 260, maxW: "calc(100vw - 24px)", bg: "var(--popover)", rounded: 12, borderWidth: 1, borderColor: "$borderColor", boxShadow: "var(--shadow-lg)", z: "var(--z-dropdown)", p: 6, gap: 2, children: [_jsx(Text, { pt: 6, px: 10, pb: 4, fontSize: "$1", fontWeight: "600", color: "$faint", textTransform: "uppercase", letterSpacing: 0.6, children: "Workspaces" }), orgs.length === 0 ? (_jsx(Text, { pt: 6, px: 10, pb: 10, fontSize: "$2", color: "$faint", children: "No workspaces on this account yet." })) : null, orgs.map((o) => {
|
|
88
|
+
const isSelected = o.id === activeOrgId || o.name === activeOrgId;
|
|
89
|
+
return (_jsxs(Button, { variant: "ghost", onClick: () => {
|
|
90
|
+
pick(o.id);
|
|
91
|
+
if (switchOrg)
|
|
92
|
+
switchOrg(o.id);
|
|
93
|
+
if (switchOrganization)
|
|
94
|
+
switchOrganization(o.id);
|
|
95
|
+
onSelect(o.id);
|
|
96
|
+
onClose();
|
|
97
|
+
}, width: "100%", justify: "space-between", px: 10, py: 6, bg: isSelected ? '$hover' : 'transparent', children: [_jsxs(XStack, { items: "center", gap: 10, minW: 0, shrink: 1, children: [_jsx(Text, { width: 24, height: 24, rounded: 6, bg: "$raised", display: "flex", items: "center", justify: "center", fontSize: "$2", fontWeight: "700", color: "$ink", shrink: 0, overflow: "hidden", children: o.logo ? (_jsx(View, { render: _jsx("img", { src: o.logo, alt: "" }), width: "100%", height: "100%", objectFit: "cover" })) : (o.displayName.toLowerCase().includes('hanzo') || o.name.toLowerCase().includes('hanzo')) ? (_jsx(HanzoMark, { size: 14 })) : (o.name.slice(0, 3).toUpperCase()) }), _jsxs(YStack, { minW: 0, shrink: 1, items: "flex-start", children: [_jsx(Text, { fontSize: "$2", fontWeight: isSelected ? '600' : '500', color: "$ink", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", children: o.displayName }), o.role ? (_jsx(Text, { fontSize: FLOOR, color: "$faint", children: o.role })) : null] })] }), isSelected ? _jsx(Check, { size: 14, color: "currentColor" }) : null] }, o.id));
|
|
98
|
+
}), _jsx(View, { height: 1, bg: "$borderColor", my: 4 }), _jsx(Button, { asChild: true, ...LINK, children: _jsxs("a", { href: iam(), target: "_blank", rel: "noreferrer", children: [_jsx(Plus, { size: 14 }), "Create Organization"] }) }), _jsx(Button, { asChild: true, ...LINK, children: _jsxs("a", { href: site("/account/organization"), children: [_jsx(Settings, { size: 14 }), "Organization Settings"] }) })] }));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* THE WORKSPACE YOU ARE STANDING IN, and the way to another one.
|
|
102
|
+
*
|
|
103
|
+
* IT NAMES THE ORG OR IT NAMES NOTHING. The label fell through to the literal
|
|
104
|
+
* "Hanzo AI" — so a reader in no org, or one whose IAM read had not landed, was
|
|
105
|
+
* told they were inside a company they may have nothing to do with, and the
|
|
106
|
+
* corner that exists to say WHERE YOU ARE was the one part of the app most
|
|
107
|
+
* confidently wrong. `selectedLocal` did the same for the id: `'hanzo'` before
|
|
108
|
+
* IAM answered, which is also the id a press would have switched to.
|
|
109
|
+
*/
|
|
110
|
+
export function TopOrgSelector() {
|
|
111
|
+
const [open, setOpen] = useState(false);
|
|
112
|
+
const buttonRef = useRef(null);
|
|
113
|
+
const { currentOrg, currentOrgId } = useOrganizations();
|
|
114
|
+
const { currentOrganization } = useAccount();
|
|
115
|
+
const activeOrg = currentOrganization || currentOrg;
|
|
116
|
+
const face = activeOrg?.logo || activeOrg?.avatar;
|
|
117
|
+
const activeOrgId = activeOrg?.name || activeOrg?.id || currentOrgId || 'hanzo';
|
|
118
|
+
const activeOrgName = activeOrg?.displayName || activeOrg?.name || 'hanzo';
|
|
119
|
+
return (_jsxs(XStack, { position: "relative", display: "inline-flex", items: "center", children: [_jsxs(Button, { ref: buttonRef, size: "sm", variant: open ? 'secondary' : 'default', onClick: () => setOpen((v) => !v), gap: 6, pl: 6, pr: 8, "aria-label": "Switch organization", children: [_jsx(XStack, { width: 20, height: 20, rounded: 9999, bg: "$raised", items: "center", justify: "center", overflow: "hidden", children: face ? (_jsx(View, { render: _jsx("img", { src: face, alt: "", width: 20, height: 20 }), width: "100%", height: "100%", objectFit: "cover" })) : (_jsx(HanzoMark, { size: 13 })) }), _jsx(Text, { fontSize: "$2", fontWeight: "600", color: "$ink", maxW: 140, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", children: activeOrgName }), _jsx(ChevronDown, { size: 13, opacity: 0.65 })] }), _jsx(OrgSwitcherDropdown, { open: open, onClose: () => setOpen(false), activeOrgId: activeOrgId, onSelect: () => setOpen(false), triggerRef: buttonRef })] }));
|
|
120
|
+
}
|
package/dist/Orgs.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export declare function Orgs({ children, front, forward, signup, }: {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
/** Home at `/`: a stranger meets the host's Landing, a member with no team `<Landing member />`. */
|
|
5
|
+
front?: boolean;
|
|
6
|
+
/** Where an open organization is sent instead of drawing `children`: /start sends it Home. */
|
|
7
|
+
forward?: string;
|
|
8
|
+
/** A stranger is shown the host's Signup rather than the sign-in: /start. */
|
|
9
|
+
signup?: boolean;
|
|
10
|
+
}): import("react").JSX.Element;
|