@minnai/create-aura-app 0.0.12 → 0.0.15
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/package.json +1 -1
- package/templates/starter/package.json +2 -2
- package/templates/starter/src/ambiance/index.ts +1 -3
- package/templates/starter/src/components/Playground/Playground.tsx +1 -1
- package/templates/starter/src/src/App.css +0 -32
- package/templates/starter/src/src/App.tsx +0 -82
- package/templates/starter/src/src/ambiance/currency-air/index.tsx +0 -25
- package/templates/starter/src/src/ambiance/currency-air/logic.ts +0 -49
- package/templates/starter/src/src/ambiance/currency-air/manifest.ts +0 -15
- package/templates/starter/src/src/ambiance/currency-air/resources.ts +0 -16
- package/templates/starter/src/src/ambiance/currency-air/ui/index.tsx +0 -42
- package/templates/starter/src/src/ambiance/index.ts +0 -48
- package/templates/starter/src/src/ambiance/stocks-air/index.ts +0 -3
- package/templates/starter/src/src/ambiance/stocks-air/index.tsx +0 -28
- package/templates/starter/src/src/ambiance/stocks-air/logic.ts +0 -87
- package/templates/starter/src/src/ambiance/stocks-air/manifest.ts +0 -15
- package/templates/starter/src/src/ambiance/stocks-air/resources.ts +0 -23
- package/templates/starter/src/src/ambiance/stocks-air/ui/index.tsx +0 -67
- package/templates/starter/src/src/assets/react.svg +0 -1
- package/templates/starter/src/src/components/AnalyticsTracker.tsx +0 -13
- package/templates/starter/src/src/components/Playground/CodeEditor.tsx +0 -121
- package/templates/starter/src/src/components/Playground/Debugger.tsx +0 -71
- package/templates/starter/src/src/components/Playground/Playground.tsx +0 -221
- package/templates/starter/src/src/components/Playground/Sidebar.tsx +0 -68
- package/templates/starter/src/src/components/ProjectSidebar/ProjectSidebar.tsx +0 -219
- package/templates/starter/src/src/components/TourGuide/TourGuide.tsx +0 -16
- package/templates/starter/src/src/components/TourGuide/index.ts +0 -1
- package/templates/starter/src/src/components/TourGuide/tour-flow.yaml +0 -137
- package/templates/starter/src/src/components/TourGuide/useTourEngine.ts +0 -376
- package/templates/starter/src/src/index.css +0 -68
- package/templates/starter/src/src/main.tsx +0 -10
- package/templates/starter/src/src/services/AnalyticsService.ts +0 -181
- package/templates/starter/src/src/types/ContextHandler.ts +0 -13
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
import { flux } from '@minnai/aura/flux/index';
|
|
3
|
-
|
|
4
|
-
// Types for tour flow
|
|
5
|
-
interface TourCommand {
|
|
6
|
-
say?: string;
|
|
7
|
-
spawn?: string;
|
|
8
|
-
props?: any;
|
|
9
|
-
wait?: string;
|
|
10
|
-
match?: string | string[];
|
|
11
|
-
air?: string;
|
|
12
|
-
onError?: string;
|
|
13
|
-
do?: string;
|
|
14
|
-
label?: string;
|
|
15
|
-
goto?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface TourStep {
|
|
19
|
-
step: string;
|
|
20
|
-
run: TourCommand[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface TourFlow {
|
|
24
|
-
name: string;
|
|
25
|
-
projectId: string;
|
|
26
|
-
steps: TourStep[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface TourState {
|
|
30
|
-
currentStep: string;
|
|
31
|
-
commandIndex: number;
|
|
32
|
-
waiting: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function useTourEngine(flow: TourFlow, projectId: string) {
|
|
36
|
-
const [state, setState] = useState<TourState>({
|
|
37
|
-
currentStep: 'IDLE',
|
|
38
|
-
commandIndex: 0,
|
|
39
|
-
waiting: false
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const initialized = useRef(false);
|
|
43
|
-
const timers = useRef<any[]>([]);
|
|
44
|
-
|
|
45
|
-
// Initialize from saved metadata
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
if (projectId !== flow.projectId) {
|
|
48
|
-
setState(s => ({ ...s, currentStep: 'DONE' }));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const unsubscribe = flux.subscribe((action: any) => {
|
|
53
|
-
if (action.type === 'METADATA_LOADED' && !initialized.current) {
|
|
54
|
-
const saved = action.payload.tourState as TourState;
|
|
55
|
-
console.log('[TourEngine] Restored:', saved || 'Starting fresh');
|
|
56
|
-
if (saved && saved.currentStep !== 'DONE') {
|
|
57
|
-
setState(saved);
|
|
58
|
-
} else {
|
|
59
|
-
setState({ currentStep: 'INIT', commandIndex: 0, waiting: false });
|
|
60
|
-
}
|
|
61
|
-
initialized.current = true;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// Fail-safe
|
|
66
|
-
const timer = setTimeout(() => {
|
|
67
|
-
if (!initialized.current) {
|
|
68
|
-
console.log('[TourEngine] No metadata, starting fresh');
|
|
69
|
-
setState({ currentStep: 'INIT', commandIndex: 0, waiting: false });
|
|
70
|
-
initialized.current = true;
|
|
71
|
-
}
|
|
72
|
-
}, 1500);
|
|
73
|
-
|
|
74
|
-
return () => {
|
|
75
|
-
unsubscribe();
|
|
76
|
-
clearTimeout(timer);
|
|
77
|
-
};
|
|
78
|
-
}, [projectId, flow.projectId]);
|
|
79
|
-
|
|
80
|
-
// Persist state
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (state.currentStep !== 'IDLE' && state.currentStep !== 'DONE') {
|
|
83
|
-
flux.dispatch({
|
|
84
|
-
type: 'SYNC_METADATA',
|
|
85
|
-
payload: { tourState: state },
|
|
86
|
-
to: 'controller'
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}, [state]);
|
|
90
|
-
|
|
91
|
-
// Say helper
|
|
92
|
-
const say = useCallback(async (text: string) => {
|
|
93
|
-
flux.dispatch({
|
|
94
|
-
type: 'SAY',
|
|
95
|
-
payload: { text, role: 'assistant' },
|
|
96
|
-
to: 'all'
|
|
97
|
-
});
|
|
98
|
-
const wordCount = text.split(/\s+/).length;
|
|
99
|
-
const delay = wordCount * 333 + 666;
|
|
100
|
-
await new Promise(resolve => {
|
|
101
|
-
const timer = setTimeout(resolve, delay);
|
|
102
|
-
timers.current.push(timer);
|
|
103
|
-
});
|
|
104
|
-
}, []);
|
|
105
|
-
|
|
106
|
-
// Execute a single command
|
|
107
|
-
const executeCommand = useCallback(async (cmd: TourCommand): Promise<string | null> => {
|
|
108
|
-
if (cmd.say) {
|
|
109
|
-
await say(cmd.say);
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (cmd.spawn) {
|
|
114
|
-
flux.dispatch({
|
|
115
|
-
type: 'ADD_CHAT_MESSAGE',
|
|
116
|
-
payload: {
|
|
117
|
-
role: 'assistant',
|
|
118
|
-
content: '',
|
|
119
|
-
attachment: { id: cmd.spawn, props: cmd.props || {} }
|
|
120
|
-
},
|
|
121
|
-
to: 'all'
|
|
122
|
-
});
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (cmd.do === 'toggle_task') {
|
|
127
|
-
flux.dispatch({
|
|
128
|
-
type: 'TOGGLE_TASK',
|
|
129
|
-
payload: { label: cmd.label, completed: true },
|
|
130
|
-
to: 'all'
|
|
131
|
-
});
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (cmd.do === 'confetti') {
|
|
136
|
-
flux.dispatch({ type: 'SHOW_CONFETTI', payload: {}, to: 'tasks-air' });
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (cmd.goto) {
|
|
141
|
-
return cmd.goto;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (cmd.wait) {
|
|
145
|
-
// Return signal to wait
|
|
146
|
-
return 'WAIT';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return null;
|
|
150
|
-
}, [say]);
|
|
151
|
-
|
|
152
|
-
// Run current step
|
|
153
|
-
useEffect(() => {
|
|
154
|
-
if (state.currentStep === 'IDLE' || state.currentStep === 'DONE' || state.waiting) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const step = flow.steps.find(s => s.step === state.currentStep);
|
|
159
|
-
if (!step) {
|
|
160
|
-
console.error('[TourEngine] Step not found:', state.currentStep);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
let cancelled = false;
|
|
165
|
-
|
|
166
|
-
const runCommands = async () => {
|
|
167
|
-
let i = state.commandIndex;
|
|
168
|
-
|
|
169
|
-
while (i < step.run.length && !cancelled) {
|
|
170
|
-
const cmd = step.run[i];
|
|
171
|
-
|
|
172
|
-
// Handle wait commands - need to pause and listen
|
|
173
|
-
if (cmd.wait) {
|
|
174
|
-
setState(s => ({ ...s, commandIndex: i, waiting: true }));
|
|
175
|
-
return; // Stop execution, wait for event
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const result = await executeCommand(cmd);
|
|
179
|
-
|
|
180
|
-
if (result && result !== 'WAIT') {
|
|
181
|
-
// goto command - switch step
|
|
182
|
-
setState({ currentStep: result, commandIndex: 0, waiting: false });
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
i++;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Reached end of step without goto
|
|
190
|
-
if (i >= step.run.length) {
|
|
191
|
-
setState(s => ({ ...s, currentStep: 'DONE' }));
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
runCommands();
|
|
196
|
-
|
|
197
|
-
return () => {
|
|
198
|
-
cancelled = true;
|
|
199
|
-
timers.current.forEach(t => clearTimeout(t));
|
|
200
|
-
timers.current = [];
|
|
201
|
-
};
|
|
202
|
-
}, [state.currentStep, state.commandIndex, state.waiting, flow, executeCommand]);
|
|
203
|
-
|
|
204
|
-
// Event listener for wait conditions
|
|
205
|
-
useEffect(() => {
|
|
206
|
-
if (!state.waiting) return;
|
|
207
|
-
|
|
208
|
-
const step = flow.steps.find(s => s.step === state.currentStep);
|
|
209
|
-
if (!step) return;
|
|
210
|
-
|
|
211
|
-
const cmd = step.run[state.commandIndex];
|
|
212
|
-
if (!cmd || !cmd.wait) return;
|
|
213
|
-
|
|
214
|
-
const unsubscribe = flux.subscribe((action: any) => {
|
|
215
|
-
let matched = false;
|
|
216
|
-
let errorStep: string | null = null;
|
|
217
|
-
|
|
218
|
-
// task_toggled - wait for TOGGLE_TASK or TASK_COMPLETED
|
|
219
|
-
if (cmd.wait === 'task_toggled') {
|
|
220
|
-
// Listen for the actual toggle/completion event
|
|
221
|
-
if (action.type === 'TOGGLE_TASK' || action.type === 'TASK_COMPLETED') {
|
|
222
|
-
const label = action.payload?.label?.toLowerCase() ||
|
|
223
|
-
action.payload?.taskLabel?.toLowerCase() || '';
|
|
224
|
-
matched = label.includes((cmd.match as string)?.toLowerCase() || '');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Also intercept CHAT_PROMPT and dispatch TOGGLE_TASK if intent matches
|
|
228
|
-
if (action.type === 'CHAT_PROMPT') {
|
|
229
|
-
const text = action.payload?.text?.toLowerCase() || '';
|
|
230
|
-
const matchStr = (cmd.match as string)?.toLowerCase() || '';
|
|
231
|
-
const toggleIntents = ['complete', 'done', 'finish', 'check', 'mark', 'toggle'];
|
|
232
|
-
const hasIntent = toggleIntents.some(i => text.includes(i));
|
|
233
|
-
const hasMatch = matchStr.split(' ').some(word => text.includes(word));
|
|
234
|
-
|
|
235
|
-
if (hasIntent && hasMatch) {
|
|
236
|
-
console.log('[TourEngine] Intercepting chat for toggle:', cmd.match);
|
|
237
|
-
// Dispatch TOGGLE_TASK - this will trigger TasksAIR which will dispatch TASK_COMPLETED
|
|
238
|
-
queueMicrotask(() => {
|
|
239
|
-
flux.dispatch({
|
|
240
|
-
type: 'TOGGLE_TASK',
|
|
241
|
-
payload: { label: cmd.match, completed: true },
|
|
242
|
-
to: 'all'
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Listen for explicit TASK_COMPLETED event
|
|
249
|
-
if (action.type === 'TASK_COMPLETED') {
|
|
250
|
-
console.log('[TourEngine] TASK_COMPLETED received:', action.payload);
|
|
251
|
-
const matchStr = (cmd.match as string)?.toLowerCase() || '';
|
|
252
|
-
const matchIndex = parseInt(matchStr, 10);
|
|
253
|
-
const isIndexMatch = !isNaN(matchIndex) && action.payload?.taskOrder === matchIndex;
|
|
254
|
-
console.log('[TourEngine] Match logic - matchStr:', matchStr, 'matchIndex:', matchIndex, 'taskOrder:', action.payload?.taskOrder, 'isIndexMatch:', isIndexMatch);
|
|
255
|
-
|
|
256
|
-
const label = action.payload?.taskLabel?.toLowerCase() || '';
|
|
257
|
-
const isTextMatch = label.includes(matchStr);
|
|
258
|
-
console.log('[TourEngine] Match logic - label:', label, 'isTextMatch:', isTextMatch);
|
|
259
|
-
|
|
260
|
-
if (isIndexMatch || isTextMatch) {
|
|
261
|
-
console.log('[TourEngine] Task completion matched!');
|
|
262
|
-
matched = true;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Listen for state updates from tasks-air (fallback/verification)
|
|
267
|
-
if (action.type === 'UPDATE_STATE' && action.payload?.airId === 'tasks-air') {
|
|
268
|
-
const tasks = action.payload?.tasks || [];
|
|
269
|
-
const matchStr = (cmd.match as string)?.toLowerCase() || '';
|
|
270
|
-
const matchIndex = parseInt(matchStr, 10);
|
|
271
|
-
|
|
272
|
-
if (!isNaN(matchIndex)) {
|
|
273
|
-
// Check by index
|
|
274
|
-
if (tasks[matchIndex] && tasks[matchIndex].completed) {
|
|
275
|
-
matched = true;
|
|
276
|
-
}
|
|
277
|
-
} else {
|
|
278
|
-
// Check by label
|
|
279
|
-
const targetTask = tasks.find((t: any) => t.label?.toLowerCase().includes(matchStr));
|
|
280
|
-
if (targetTask && targetTask.completed) {
|
|
281
|
-
matched = true;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// task_added - wait for ADD_TASK or TASK_ADDED
|
|
288
|
-
if (cmd.wait === 'task_added') {
|
|
289
|
-
if (action.type === 'ADD_TASK' || action.type === 'TASK_ADDED') {
|
|
290
|
-
const label = action.payload?.label?.toLowerCase() || '';
|
|
291
|
-
matched = label.includes((cmd.match as string)?.toLowerCase() || '');
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Intercept CHAT_PROMPT for add task intent
|
|
295
|
-
if (action.type === 'CHAT_PROMPT') {
|
|
296
|
-
const text = action.payload?.text?.toLowerCase() || '';
|
|
297
|
-
const addIntents = ['add task', 'add a task', 'create task', 'new task'];
|
|
298
|
-
const hasIntent = addIntents.some(i => text.includes(i));
|
|
299
|
-
|
|
300
|
-
if (hasIntent) {
|
|
301
|
-
// Extract task label from text (after "add task:" or similar)
|
|
302
|
-
const colonIdx = text.indexOf(':');
|
|
303
|
-
const taskLabel = colonIdx > -1 ? text.substring(colonIdx + 1).trim() : text.replace(/add\s*(a\s*)?task\s*/i, '').trim();
|
|
304
|
-
|
|
305
|
-
if (taskLabel) {
|
|
306
|
-
console.log('[TourEngine] Intercepting chat for add task:', taskLabel);
|
|
307
|
-
// Check if added task matches what we're waiting for
|
|
308
|
-
if (taskLabel.includes((cmd.match as string)?.toLowerCase() || '')) {
|
|
309
|
-
matched = true;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// air_spawned - wait for AIR to appear in Space
|
|
317
|
-
if (cmd.wait === 'air_spawned') {
|
|
318
|
-
console.log('[TourEngine] Waiting for air_spawned:', cmd.air, 'Got action:', action.type);
|
|
319
|
-
const types = ['SPAWN_AIR', 'WINDOW_SPAWNED', 'ADD_WINDOW', 'ADD_CHAT_MESSAGE'];
|
|
320
|
-
if (types.includes(action.type)) {
|
|
321
|
-
const p = action.payload;
|
|
322
|
-
const ids = [p.id, p.manifestId, p.props?.id, p.attachment?.id].filter(Boolean);
|
|
323
|
-
console.log('[TourEngine] Checking ids:', ids, 'for air:', cmd.air);
|
|
324
|
-
matched = ids.some((id: string) => id.includes(cmd.air || ''));
|
|
325
|
-
if (matched) console.log('[TourEngine] air_spawned matched!');
|
|
326
|
-
}
|
|
327
|
-
// Also check controller state response
|
|
328
|
-
if (action.type === 'CONTROLLER_STATE') {
|
|
329
|
-
const windows = action.payload.windows || [];
|
|
330
|
-
console.log('[TourEngine] Checking CONTROLLER_STATE windows:', windows);
|
|
331
|
-
matched = windows.some((w: any) =>
|
|
332
|
-
w.manifestId?.includes(cmd.air || '') || w.id?.includes(cmd.air || '')
|
|
333
|
-
);
|
|
334
|
-
if (matched) console.log('[TourEngine] air_spawned matched from CONTROLLER_STATE!');
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// air_success - wait for UPDATE_STATE from specific AIR
|
|
339
|
-
if (cmd.wait === 'air_success') {
|
|
340
|
-
console.log('[TourEngine] Waiting for air_success:', cmd.air, 'Got action:', action.type, action.payload);
|
|
341
|
-
if (action.type === 'AIR_ERROR' && action.payload.airId?.includes(cmd.air || '')) {
|
|
342
|
-
console.log('[TourEngine] AIR_ERROR detected, jumping to error step:', cmd.onError);
|
|
343
|
-
errorStep = cmd.onError || null;
|
|
344
|
-
}
|
|
345
|
-
if (action.type === 'UPDATE_STATE' && action.payload.airId?.includes(cmd.air || '')) {
|
|
346
|
-
console.log('[TourEngine] air_success matched!');
|
|
347
|
-
matched = true;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (errorStep) {
|
|
352
|
-
setState({ currentStep: errorStep, commandIndex: 0, waiting: false });
|
|
353
|
-
} else if (matched) {
|
|
354
|
-
setState(s => ({
|
|
355
|
-
...s,
|
|
356
|
-
commandIndex: s.commandIndex + 1,
|
|
357
|
-
waiting: false
|
|
358
|
-
}));
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// For air_spawned, request controller state to check if already in space
|
|
363
|
-
// Also immediately check if it's already spawned by looking at recent actions
|
|
364
|
-
if (cmd.wait === 'air_spawned') {
|
|
365
|
-
flux.dispatch({ type: 'REQUEST_CONTROLLER_STATE', payload: {}, to: 'controller' });
|
|
366
|
-
// Give a small delay to ensure the air has time to mount and dispatch events
|
|
367
|
-
setTimeout(() => {
|
|
368
|
-
flux.dispatch({ type: 'REQUEST_CONTROLLER_STATE', payload: {}, to: 'controller' });
|
|
369
|
-
}, 300);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return unsubscribe;
|
|
373
|
-
}, [state.waiting, state.currentStep, state.commandIndex, flow]);
|
|
374
|
-
|
|
375
|
-
return state;
|
|
376
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
3
|
-
line-height: 1.5;
|
|
4
|
-
font-weight: 400;
|
|
5
|
-
|
|
6
|
-
color-scheme: light dark;
|
|
7
|
-
color: rgba(255, 255, 255, 0.87);
|
|
8
|
-
background-color: #242424;
|
|
9
|
-
|
|
10
|
-
font-synthesis: none;
|
|
11
|
-
text-rendering: optimizeLegibility;
|
|
12
|
-
-webkit-font-smoothing: antialiased;
|
|
13
|
-
-moz-osx-font-smoothing: grayscale;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
a {
|
|
17
|
-
font-weight: 500;
|
|
18
|
-
color: #646cff;
|
|
19
|
-
text-decoration: inherit;
|
|
20
|
-
}
|
|
21
|
-
a:hover {
|
|
22
|
-
color: #535bf2;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
body {
|
|
26
|
-
margin: 0;
|
|
27
|
-
display: flex;
|
|
28
|
-
place-items: center;
|
|
29
|
-
min-width: 320px;
|
|
30
|
-
min-height: 100vh;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
h1 {
|
|
34
|
-
font-size: 3.2em;
|
|
35
|
-
line-height: 1.1;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
button {
|
|
39
|
-
border-radius: 8px;
|
|
40
|
-
border: 1px solid transparent;
|
|
41
|
-
padding: 0.6em 1.2em;
|
|
42
|
-
font-size: 1em;
|
|
43
|
-
font-weight: 500;
|
|
44
|
-
font-family: inherit;
|
|
45
|
-
background-color: #1a1a1a;
|
|
46
|
-
cursor: pointer;
|
|
47
|
-
transition: border-color 0.25s;
|
|
48
|
-
}
|
|
49
|
-
button:hover {
|
|
50
|
-
border-color: #646cff;
|
|
51
|
-
}
|
|
52
|
-
button:focus,
|
|
53
|
-
button:focus-visible {
|
|
54
|
-
outline: 4px auto -webkit-focus-ring-color;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
@media (prefers-color-scheme: light) {
|
|
58
|
-
:root {
|
|
59
|
-
color: #213547;
|
|
60
|
-
background-color: #ffffff;
|
|
61
|
-
}
|
|
62
|
-
a:hover {
|
|
63
|
-
color: #747bff;
|
|
64
|
-
}
|
|
65
|
-
button {
|
|
66
|
-
background-color: #f9f9f9;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { initializeApp } from "firebase/app";
|
|
2
|
-
import { getAnalytics, logEvent as firebaseLogEvent, Analytics } from "firebase/analytics";
|
|
3
|
-
import { getPerformance, trace, FirebasePerformance, PerformanceTrace } from "firebase/performance";
|
|
4
|
-
import { flux } from '@minnai/aura/flux/index';
|
|
5
|
-
|
|
6
|
-
// Community Firebase Config (fallback when user doesn't provide their own)
|
|
7
|
-
const communityFirebaseConfig = {
|
|
8
|
-
apiKey: "AIzaSyBxP1CkhBFaHFDX9HcVQurcBDVzzFruc4k",
|
|
9
|
-
authDomain: "saga-9dd98.firebaseapp.com",
|
|
10
|
-
projectId: "saga-9dd98",
|
|
11
|
-
storageBucket: "saga-9dd98.firebasestorage.app",
|
|
12
|
-
messagingSenderId: "723636478082",
|
|
13
|
-
appId: "1:723636478082:web:68b2fa0eb2010706e2549b",
|
|
14
|
-
measurementId: "G-67DEXLGRDQ"
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
declare global {
|
|
18
|
-
interface Window {
|
|
19
|
-
__ANALYTICS_LOGS__: any[];
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class AnalyticsService {
|
|
24
|
-
private analytics: Analytics | null = null;
|
|
25
|
-
private performance: FirebasePerformance | null = null;
|
|
26
|
-
private currentTrace: PerformanceTrace | null = null;
|
|
27
|
-
private ttvTrace: PerformanceTrace | null = null;
|
|
28
|
-
private TTV_START_KEY = 'aura_ttv_start';
|
|
29
|
-
|
|
30
|
-
constructor(userConfig?: any) {
|
|
31
|
-
// 1. Try User Config (Environment Variables)
|
|
32
|
-
const envConfig = {
|
|
33
|
-
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
|
34
|
-
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
|
|
35
|
-
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
|
36
|
-
appId: import.meta.env.VITE_FIREBASE_APP_ID,
|
|
37
|
-
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const configToUse = (envConfig.apiKey) ? envConfig : (userConfig || communityFirebaseConfig);
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
if (configToUse.apiKey && !configToUse.apiKey.includes("Dummy")) {
|
|
44
|
-
const app = initializeApp(configToUse);
|
|
45
|
-
this.analytics = getAnalytics(app);
|
|
46
|
-
this.performance = getPerformance(app);
|
|
47
|
-
console.log("[Analytics] Service Initialized (" + (envConfig.apiKey ? "User" : "Community") + ")");
|
|
48
|
-
} else {
|
|
49
|
-
console.log("[Analytics] No valid config found. Telemetry disabled.");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Always setup listeners for local debugging/TTV, or if analytics is active
|
|
53
|
-
if (this.analytics || import.meta.env.DEV) {
|
|
54
|
-
this.setupListeners();
|
|
55
|
-
}
|
|
56
|
-
} catch (e) {
|
|
57
|
-
console.warn("[Analytics] Initialization failed", e);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private setupListeners() {
|
|
62
|
-
// Listen to Aura Signals via Flux
|
|
63
|
-
flux.subscribe((msg: any) => {
|
|
64
|
-
// 1. Trace Generation Latency
|
|
65
|
-
if (msg.type === 'CHAT_PROMPT') {
|
|
66
|
-
// Stop existing if any (user spammed send)
|
|
67
|
-
this.stopTrace(this.currentTrace);
|
|
68
|
-
this.currentTrace = this.startTrace('air_generation_latency');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 2. Stop Trace on AIR Spawn (Success)
|
|
72
|
-
if (msg.type === 'SPAWN_AIR' || msg.type === 'WINDOW_SPAWNED') {
|
|
73
|
-
this.log('air_generated', {
|
|
74
|
-
air_type: msg.payload.id || msg.payload.manifestId,
|
|
75
|
-
success: true
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Stop Generation Trace
|
|
79
|
-
if (this.currentTrace) {
|
|
80
|
-
this.currentTrace.putAttribute('air_id', msg.payload.id || msg.payload.manifestId || 'unknown');
|
|
81
|
-
this.stopTrace(this.currentTrace);
|
|
82
|
-
this.currentTrace = null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
this.completeTTVTimer();
|
|
86
|
-
}
|
|
87
|
-
// We can add more listeners here (e.g. Errors)
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
public log(eventName: string, params?: any) {
|
|
92
|
-
// Log to local global array for verification
|
|
93
|
-
if (!window.__ANALYTICS_LOGS__) {
|
|
94
|
-
window.__ANALYTICS_LOGS__ = [];
|
|
95
|
-
}
|
|
96
|
-
window.__ANALYTICS_LOGS__.push({ event: eventName, params, timestamp: Date.now() });
|
|
97
|
-
|
|
98
|
-
// Always log in DEV mode for visibility, even if Analytics is disabled/misconfigured
|
|
99
|
-
if (import.meta.env.DEV) {
|
|
100
|
-
console.log(`[Analytics] ${this.analytics ? 'Sent' : 'Skipped (No Key)'}: ${eventName}`, params);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (this.analytics) {
|
|
104
|
-
try {
|
|
105
|
-
firebaseLogEvent(this.analytics, eventName, params);
|
|
106
|
-
} catch (e) {
|
|
107
|
-
// Ignore send errors
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
public startTrace(name: string): PerformanceTrace | null {
|
|
113
|
-
if (this.performance) {
|
|
114
|
-
const t = trace(this.performance, name);
|
|
115
|
-
t.start();
|
|
116
|
-
if (import.meta.env.DEV) {
|
|
117
|
-
console.log(`[Analytics] Trace Started: ${name}`);
|
|
118
|
-
}
|
|
119
|
-
return t;
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
public stopTrace(t: PerformanceTrace | null) {
|
|
125
|
-
if (t) {
|
|
126
|
-
t.stop();
|
|
127
|
-
if (import.meta.env.DEV) {
|
|
128
|
-
console.log(`[Analytics] Trace Stopped: ${t.getAttribute('name') || 'unknown'}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
public startTTVTimer() {
|
|
134
|
-
if (!sessionStorage.getItem(this.TTV_START_KEY)) {
|
|
135
|
-
const now = Date.now();
|
|
136
|
-
sessionStorage.setItem(this.TTV_START_KEY, now.toString());
|
|
137
|
-
console.log('[Analytics] TTV Timer Started at', now);
|
|
138
|
-
|
|
139
|
-
// Start Trace for TTV
|
|
140
|
-
this.ttvTrace = this.startTrace('time_to_value_trace');
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
public completeTTVTimer() {
|
|
145
|
-
const startStr = sessionStorage.getItem(this.TTV_START_KEY);
|
|
146
|
-
if (startStr) {
|
|
147
|
-
const start = parseInt(startStr, 10);
|
|
148
|
-
const end = Date.now();
|
|
149
|
-
const duration = end - start;
|
|
150
|
-
|
|
151
|
-
this.log('time_to_value', { duration_ms: duration, duration_sec: duration / 1000 });
|
|
152
|
-
console.log(`[Analytics] TTV Complete: ${duration}ms`);
|
|
153
|
-
|
|
154
|
-
// Stop Trace for TTV
|
|
155
|
-
if (this.ttvTrace) {
|
|
156
|
-
this.ttvTrace.putAttribute('duration_sec', (duration / 1000).toString());
|
|
157
|
-
this.stopTrace(this.ttvTrace);
|
|
158
|
-
this.ttvTrace = null;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Clear so we only track first time to value per session/refresh logic if desired
|
|
162
|
-
// For now, let's keep it to allow multiple measurements or clear it?
|
|
163
|
-
// Usually TTV is once per session.
|
|
164
|
-
sessionStorage.removeItem(this.TTV_START_KEY);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
public logLogin(method: string) {
|
|
169
|
-
this.log('login', { method });
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
public logSignup(method: string) {
|
|
173
|
-
this.log('sign_up', { method });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
public logPageView(page_path: string) {
|
|
177
|
-
this.log('page_view', { page_path });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export const analyticsService = new AnalyticsService();
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export interface ContextHandler {
|
|
2
|
-
/**
|
|
3
|
-
* Returns the current context/state of the AIR.
|
|
4
|
-
* This is used by the Intent Planner to understand what the user is looking at.
|
|
5
|
-
*/
|
|
6
|
-
getContext(): Promise<any>;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* List of capabilities this AIR supports.
|
|
10
|
-
* e.g. ['text-editing', 'video-playback', 'market-data']
|
|
11
|
-
*/
|
|
12
|
-
capabilities: string[];
|
|
13
|
-
}
|