ugly-app 0.1.118 → 0.1.119
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/README.md +273 -19
- package/coverage/client/AppProvider.tsx.html +1 -1
- package/coverage/client/FeedbackContext.ts.html +1 -1
- package/coverage/client/LoginPopup.tsx.html +1 -1
- package/coverage/client/Router.tsx.html +1 -1
- package/coverage/client/Screenshot.ts.html +1 -1
- package/coverage/client/ViewFlipper.tsx.html +1 -1
- package/coverage/client/animation/Animated.tsx.html +1 -1
- package/coverage/client/animation/animatedValue.ts.html +1 -1
- package/coverage/client/animation/index.html +1 -1
- package/coverage/client/audio/index.html +1 -1
- package/coverage/client/audio/useSTT.ts.html +2 -2
- package/coverage/client/audio/useTTS.ts.html +1 -1
- package/coverage/client/components/Button.tsx.html +1 -1
- package/coverage/client/components/FeedbackButton.tsx.html +1 -1
- package/coverage/client/components/Input.tsx.html +1 -1
- package/coverage/client/components/Modal.tsx.html +1 -1
- package/coverage/client/components/Text.tsx.html +1 -1
- package/coverage/client/components/Toast.tsx.html +1 -1
- package/coverage/client/components/index.html +1 -1
- package/coverage/client/components/zIndex.ts.html +1 -1
- package/coverage/client/createSocket.ts.html +48 -48
- package/coverage/client/index.html +1 -1
- package/coverage/clover.xml +561 -375
- package/coverage/coverage-final.json +22 -16
- package/coverage/index.html +42 -42
- package/coverage/server/Auth.ts.html +1 -1
- package/coverage/server/Cache.ts.html +1 -1
- package/coverage/server/DB.ts.html +1 -1
- package/coverage/server/Email.ts.html +1 -1
- package/coverage/server/EmailTemplate.ts.html +1 -1
- package/coverage/server/PushNotification.ts.html +1 -1
- package/coverage/server/RateLimit.ts.html +1 -1
- package/coverage/server/Router.ts.html +1 -1
- package/coverage/server/Socket.ts.html +1 -1
- package/coverage/server/Storage.ts.html +1 -1
- package/coverage/server/StoreHandlers.ts.html +1 -1
- package/coverage/server/ai/ImageGenClient.ts.html +1 -1
- package/coverage/server/ai/ProviderSelector.ts.html +44 -8
- package/coverage/server/ai/TextGenClient.ts.html +1 -1
- package/coverage/server/ai/WebSearchClient.ts.html +322 -0
- package/coverage/server/ai/fallbacks.ts.html +1 -1
- package/coverage/server/ai/index.html +29 -14
- package/coverage/server/ai/index.ts.html +31 -4
- package/coverage/server/ai/providers/Claude.ts.html +1 -1
- package/coverage/server/ai/providers/FAL.ts.html +1 -1
- package/coverage/server/ai/providers/Fireworks.ts.html +1 -1
- package/coverage/server/ai/providers/Google.ts.html +1 -1
- package/coverage/server/ai/providers/GoogleImage.ts.html +1 -1
- package/coverage/server/ai/providers/Groq.ts.html +1 -1
- package/coverage/server/ai/providers/Kagi.ts.html +505 -0
- package/coverage/server/ai/providers/Kie.ts.html +1 -1
- package/coverage/server/ai/providers/KieImage.ts.html +1 -1
- package/coverage/server/ai/providers/OpenAIText.ts.html +1 -1
- package/coverage/server/ai/providers/Together.ts.html +1 -1
- package/coverage/server/ai/providers/TogetherImage.ts.html +1 -1
- package/coverage/server/ai/providers/UglyBotImageGenProvider.ts.html +1 -1
- package/coverage/server/ai/providers/UglyBotTextGenProvider.ts.html +1 -1
- package/coverage/server/ai/providers/UglyBotWebSearchProvider.ts.html +412 -0
- package/coverage/server/ai/providers/Wavespeed.ts.html +2 -2
- package/coverage/server/ai/providers/index.html +39 -9
- package/coverage/server/ai/registry.ts.html +61 -10
- package/coverage/server/ai/types.ts.html +85 -4
- package/coverage/server/audio/STTStream.ts.html +1 -1
- package/coverage/server/audio/TTSStream.ts.html +1 -1
- package/coverage/server/audio/index.html +1 -1
- package/coverage/server/audio/index.ts.html +1 -1
- package/coverage/server/audio/resample.ts.html +1 -1
- package/coverage/server/audio/stt/GroqWhisper.ts.html +37 -4
- package/coverage/server/audio/stt/index.html +5 -5
- package/coverage/server/audio/stt/registry.ts.html +1 -1
- package/coverage/server/audio/tts/InWorld.ts.html +5 -2
- package/coverage/server/audio/tts/index.html +1 -1
- package/coverage/server/audio/tts/registry.ts.html +1 -1
- package/coverage/server/billing/BillingGateway.ts.html +273 -33
- package/coverage/server/billing/BillingLedger.ts.html +39 -39
- package/coverage/server/billing/CreditStore.ts.html +24 -24
- package/coverage/server/billing/LimitEnforcer.ts.html +57 -57
- package/coverage/server/billing/UserLimitCache.ts.html +27 -27
- package/coverage/server/billing/index.html +23 -23
- package/coverage/server/billing/types.ts.html +401 -11
- package/coverage/server/embeddings/EmbeddingClient.ts.html +1 -1
- package/coverage/server/embeddings/index.html +1 -1
- package/coverage/server/embeddings/providers/OpenAI.ts.html +1 -1
- package/coverage/server/embeddings/providers/index.html +1 -1
- package/coverage/server/embeddings/registry.ts.html +1 -1
- package/coverage/server/index.html +1 -1
- package/coverage/shared/Api.ts.html +1 -1
- package/coverage/shared/Audio.ts.html +1318 -0
- package/coverage/shared/DB.ts.html +1 -1
- package/coverage/shared/Errors.ts.html +1 -1
- package/coverage/shared/Experiment.ts.html +1 -1
- package/coverage/shared/ImageGen.ts.html +1 -1
- package/coverage/shared/Router.ts.html +1 -1
- package/coverage/shared/TextGen.ts.html +1 -1
- package/coverage/shared/Voice.ts.html +343 -0
- package/coverage/shared/WebSearch.ts.html +169 -0
- package/coverage/shared/index.html +53 -8
- package/coverage/shared/index.ts.html +141 -3
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/client/audio/BlobAudioAnalyzer.js.map +1 -1
- package/dist/client/audio/CircleVisualizer.js.map +1 -1
- package/dist/client/audio/MicVisualizer.js.map +1 -1
- package/dist/client/audio/WaveVisualizer.js.map +1 -1
- package/dist/client/audio/blob-styles/MetaballBlobStyle.js.map +1 -1
- package/dist/client/audio/blob-styles/OrbsBlobStyle.js.map +1 -1
- package/dist/client/audio/blob-styles/OrganicBlobStyle.js.map +1 -1
- package/dist/client/audio/blob-styles/ParticleBlobStyle.js.map +1 -1
- package/dist/client/audio/blob-styles/SiriBlobStyle.js.map +1 -1
- package/dist/server/audio/stt/AudioStreamProcessor.js.map +1 -1
- package/dist/server/audio/stt/Deepgram.d.ts.map +1 -1
- package/dist/server/audio/stt/Deepgram.js +2 -1
- package/dist/server/audio/stt/Deepgram.js.map +1 -1
- package/dist/server/audio/stt/PyannoteSegmentation.js.map +1 -1
- package/dist/server/audio/stt/SileroVAD.js.map +1 -1
- package/dist/server/audio/tts/LipSync.js.map +1 -1
- package/dist/server/audio/tts/LipSyncJa.js.map +1 -1
- package/dist/server/audio/tts/LipSyncZh.js.map +1 -1
- package/dist/server/audio/tts/TextToSpeech.js.map +1 -1
- package/dist/server/audio/tts/TextToSpeechStream.js.map +1 -1
- package/dist/server/audio/voice/index.js.map +1 -1
- package/dist/shared/Audio.d.ts.map +1 -1
- package/dist/shared/Audio.js +6 -8
- package/dist/shared/Audio.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/version.ts +1 -1
- package/src/client/audio/BlobAudioAnalyzer.ts +2 -2
- package/src/client/audio/CircleVisualizer.ts +2 -2
- package/src/client/audio/MicVisualizer.tsx +3 -3
- package/src/client/audio/WaveVisualizer.ts +1 -1
- package/src/client/audio/blob-styles/MetaballBlobStyle.ts +8 -8
- package/src/client/audio/blob-styles/OrbsBlobStyle.ts +8 -8
- package/src/client/audio/blob-styles/OrganicBlobStyle.ts +8 -8
- package/src/client/audio/blob-styles/ParticleBlobStyle.ts +10 -10
- package/src/client/audio/blob-styles/SiriBlobStyle.ts +11 -11
- package/src/server/audio/stt/AudioStreamProcessor.ts +1 -1
- package/src/server/audio/stt/Deepgram.ts +5 -4
- package/src/server/audio/stt/PyannoteSegmentation.ts +1 -1
- package/src/server/audio/stt/SileroVAD.ts +1 -1
- package/src/server/audio/tts/LipSync.ts +9 -9
- package/src/server/audio/tts/LipSyncJa.ts +5 -5
- package/src/server/audio/tts/LipSyncZh.ts +3 -3
- package/src/server/audio/tts/TextToSpeech.ts +6 -6
- package/src/server/audio/tts/TextToSpeechStream.ts +6 -6
- package/src/server/audio/voice/index.ts +2 -2
- package/src/shared/Audio.ts +6 -10
package/README.md
CHANGED
|
@@ -8,8 +8,10 @@ A full-stack TypeScript framework for building production-ready web applications
|
|
|
8
8
|
- **Client**: React + Vite with typed routing, lazy pages, and popup management
|
|
9
9
|
- **Database**: MongoDB with typed collections, dot-notation updates, indexes, migrations, and live document tracking
|
|
10
10
|
- **Auth**: JWT + HttpOnly cookies, ugly.bot OAuth out of the box, extensible via `AuthProvider`
|
|
11
|
-
- **AI**: Text generation (Together, Claude, OpenAI, Google, Groq, Fireworks) + image generation (Together, FAL, Google, Wavespeed) + embeddings + STT/TTS
|
|
11
|
+
- **AI**: Text generation (Together, Claude, OpenAI, Google, Groq, Fireworks, Kie) + image generation (Together, FAL, Google, Wavespeed, Kie) + embeddings + STT/TTS
|
|
12
12
|
- **Storage**: Cloudflare R2 / AWS S3 with presigned uploads
|
|
13
|
+
- **Web search**: Kagi and UglyBot providers with search, summarize, and enrich
|
|
14
|
+
- **Billing**: Usage-based billing with per-user/global limits, credits, and threshold alerts
|
|
13
15
|
- **CLI**: `ugly-app` commands for dev, build, deploy, migrations, logs, and auth utilities
|
|
14
16
|
|
|
15
17
|
## Quick start
|
|
@@ -454,7 +456,7 @@ import {
|
|
|
454
456
|
SlideFromRight,
|
|
455
457
|
} from 'ugly-app/client';
|
|
456
458
|
|
|
457
|
-
// Create an animated value (0
|
|
459
|
+
// Create an animated value (0->1 spring)
|
|
458
460
|
const spring = createAnimatedValue(0);
|
|
459
461
|
spring.start(1, { duration: 300, easing: easingFunctions.easeOut });
|
|
460
462
|
|
|
@@ -489,6 +491,10 @@ const anim = useAnimatedValue(0);
|
|
|
489
491
|
|
|
490
492
|
Available easings: `easingFunctions.linear`, `easeIn`, `easeOut`, `easeInOut`, `springGentle`, `springSnappy`, `springBouncy`, `slow`.
|
|
491
493
|
|
|
494
|
+
Additional animation hooks:
|
|
495
|
+
- `useAnimatedTransition` / `useAnimatedPresence` — mount/unmount transitions with phase tracking
|
|
496
|
+
- `useScrollAnimation` / `useStaggerAnimation` — scroll-driven and staggered entrance animations
|
|
497
|
+
|
|
492
498
|
#### Screenshot capture
|
|
493
499
|
|
|
494
500
|
```typescript
|
|
@@ -501,7 +507,7 @@ const dataUrl = await captureScreenshot(); // captures the current viewport
|
|
|
501
507
|
|
|
502
508
|
`ugly-app/client` exports a set of built-in UI components:
|
|
503
509
|
|
|
504
|
-
`Button`, `Card`, `EnumInput`, `Header`, `Image`, `Input`, `Modal`, `PageLayout`, `Panel`, `PopupPanel`, `Pressable`, `ScrollView`, `SettingGroup`, `Text`, `Toast`, `View`
|
|
510
|
+
`Button`, `Card`, `EnumInput`, `Header`, `Image`, `Input`, `Modal`, `PageLayout`, `Panel`, `PopupPanel`, `Pressable`, `ScrollView`, `SettingGroup`, `Text`, `Toast`, `View`, `ResponsiveGrid`, `TabPicker`, `HeaderTabPicker`, `TabContent`, `TabContentAllActive`
|
|
505
511
|
|
|
506
512
|
---
|
|
507
513
|
|
|
@@ -707,17 +713,111 @@ const embeddings = createEmbeddingClient();
|
|
|
707
713
|
const similarity = cosineSimilarity(vectorA, vectorB);
|
|
708
714
|
```
|
|
709
715
|
|
|
710
|
-
### Speech-to-text
|
|
716
|
+
### Speech-to-text (STT)
|
|
717
|
+
|
|
718
|
+
Server-side STT uses a provider registry pattern. Providers auto-register when their API key env var is set.
|
|
719
|
+
|
|
720
|
+
```typescript
|
|
721
|
+
import { registerSTTProvider, selectSTTProvider, getAllSTTProviders } from 'ugly-app';
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**Built-in STT providers:**
|
|
725
|
+
|
|
726
|
+
| Provider | Import | Env var | Mode |
|
|
727
|
+
|----------|--------|---------|------|
|
|
728
|
+
| Deepgram (nova-2) | `deepgramSTTProvider` | `DEEPGRAM_API_KEY` | Real-time streaming via WebSocket |
|
|
729
|
+
| OpenAI Whisper | `openAIWhisperSTTProvider` | `OPENAI_API_KEY` | Batch (buffers audio, transcribes on stop) |
|
|
730
|
+
| Groq Whisper | `groqWhisperSTTProvider` | `GROQ_API_KEY` | Batch |
|
|
731
|
+
|
|
732
|
+
**STT provider interface:**
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
interface STTProvider {
|
|
736
|
+
name: string;
|
|
737
|
+
apiKeyEnv: string;
|
|
738
|
+
connect(
|
|
739
|
+
onTranscript: (result: STTTranscript) => void,
|
|
740
|
+
onError: (err: string) => void,
|
|
741
|
+
lang?: string,
|
|
742
|
+
options?: STTConnectOptions,
|
|
743
|
+
onUsage?: (report: STTUsageReport) => void,
|
|
744
|
+
): Promise<STTSession>;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
interface STTSession {
|
|
748
|
+
sendAudio(pcm16: Buffer): void; // PCM16 at 16kHz mono
|
|
749
|
+
stop(): Promise<void>;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
interface STTTranscript { text: string; isFinal: boolean; lang?: string; words?: STTWord[] }
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
**Client-side hook:**
|
|
756
|
+
|
|
757
|
+
```typescript
|
|
758
|
+
import { useSTT } from 'ugly-app/client';
|
|
759
|
+
const { start, stop, transcript, isListening } = useSTT(socket, options);
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### Text-to-speech (TTS)
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
import { registerTTSProvider, selectTTSProvider, azureTTSProvider } from 'ugly-app';
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
**Built-in TTS provider:**
|
|
769
|
+
|
|
770
|
+
| Provider | Import | Env vars |
|
|
771
|
+
|----------|--------|----------|
|
|
772
|
+
| Azure TTS | `azureTTSProvider` | `AZURE_TTS_KEY`, `AZURE_TTS_REGION` |
|
|
773
|
+
|
|
774
|
+
**TTS provider interface:**
|
|
775
|
+
|
|
776
|
+
```typescript
|
|
777
|
+
interface TTSProvider {
|
|
778
|
+
name: string;
|
|
779
|
+
apiKeyEnv: string;
|
|
780
|
+
stream(text: string, voice: string, options?: TTSStreamOptions): AsyncGenerator<TTSChunk>;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
interface TTSChunk {
|
|
784
|
+
audio: Buffer; // PCM16, mono, 24kHz
|
|
785
|
+
word?: string;
|
|
786
|
+
startMs?: number;
|
|
787
|
+
durationMs?: number;
|
|
788
|
+
visemes?: TTSViseme[]; // When requestVisemes=true (for lip sync)
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
**Client-side hook:**
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
import { useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### Web search
|
|
711
799
|
|
|
712
800
|
```typescript
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
import { registerTTSProvider, azureTTSProvider } from 'ugly-app';
|
|
801
|
+
import { createWebSearchClient, registerWebSearchProvider } from 'ugly-app';
|
|
802
|
+
const search = createWebSearchClient(userId);
|
|
716
803
|
|
|
717
|
-
|
|
718
|
-
|
|
804
|
+
const results = await search.search({ query: 'hello', limit: 10 });
|
|
805
|
+
const summary = await search.summarize({ url: 'https://...' });
|
|
806
|
+
const web = await search.enrichWeb({ query: 'topic' });
|
|
807
|
+
const news = await search.enrichNews({ query: 'topic' });
|
|
719
808
|
```
|
|
720
809
|
|
|
810
|
+
**`WebSearchClient` methods:**
|
|
811
|
+
|
|
812
|
+
| Method | Description |
|
|
813
|
+
|--------|-------------|
|
|
814
|
+
| `search({ query, limit? })` | Web search — returns `{ items, related? }` |
|
|
815
|
+
| `summarize({ url?, text? })` | Summarize a URL or text — returns summary string |
|
|
816
|
+
| `enrichWeb({ query })` | Enriched web results |
|
|
817
|
+
| `enrichNews({ query })` | Enriched news results |
|
|
818
|
+
|
|
819
|
+
Built-in providers: Kagi (`KAGI_API_KEY`) and UglyBot.
|
|
820
|
+
|
|
721
821
|
### Custom providers
|
|
722
822
|
|
|
723
823
|
```typescript
|
|
@@ -777,6 +877,135 @@ Static build-time assets go in `client/public/`. Never hardcode `/asset/...` pat
|
|
|
777
877
|
|
|
778
878
|
---
|
|
779
879
|
|
|
880
|
+
## Billing
|
|
881
|
+
|
|
882
|
+
Usage-based billing with per-user limits, global provider limits, pre-paid credits, and threshold alerts.
|
|
883
|
+
|
|
884
|
+
```typescript
|
|
885
|
+
import { initBillingGateway, getBillingGateway } from 'ugly-app';
|
|
886
|
+
|
|
887
|
+
const billing = initBillingGateway(db, {
|
|
888
|
+
global: { hourlyUsd: 100, thresholdPct: 0.8 },
|
|
889
|
+
providers: {
|
|
890
|
+
openai: { hourlyUsd: 50, thresholdPct: 0.9 },
|
|
891
|
+
},
|
|
892
|
+
profitMarginPct: 0.2, // 20% markup on costs
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
// Charge a user
|
|
896
|
+
await billing.charge({
|
|
897
|
+
userId: '...',
|
|
898
|
+
provider: 'openai',
|
|
899
|
+
model: 'gpt-4o',
|
|
900
|
+
type: 'textGen', // 'textGen' | 'imageGen' | 'stt' | 'tts' | 'embedding' | 'service'
|
|
901
|
+
costUsd: 0.03,
|
|
902
|
+
inputTokens: 1000,
|
|
903
|
+
outputTokens: 500,
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// Pre-flight check
|
|
907
|
+
const canPay = await billing.canCharge(userId, 0.05);
|
|
908
|
+
|
|
909
|
+
// Grant credits
|
|
910
|
+
await billing.grantCredit(userId, 10.00);
|
|
911
|
+
|
|
912
|
+
// Query spend
|
|
913
|
+
const usage = await billing.getSpend(userId, { from, to });
|
|
914
|
+
|
|
915
|
+
// Threshold callbacks
|
|
916
|
+
billing.setUserThresholdCallback((userId, period, spend, limit) => { /* alert */ });
|
|
917
|
+
billing.setGlobalThresholdCallback((period, spend, limit) => { /* alert */ });
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
**Per-user limits** are resolved via `setUserLimitHook()`:
|
|
921
|
+
|
|
922
|
+
```typescript
|
|
923
|
+
billing.setUserLimitHook(async (userId) => ({
|
|
924
|
+
hourlyUsd: 5,
|
|
925
|
+
dailyUsd: 50,
|
|
926
|
+
weeklyUsd: 200,
|
|
927
|
+
thresholds: { hourly: 0.8, daily: 0.9, weekly: 0.95 },
|
|
928
|
+
}));
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
The billing state machine tracks spend across hourly, daily, and weekly windows. Global and per-provider limits are always enforced; user credits act as a fallback when user limits are exceeded but never bypass global limits.
|
|
932
|
+
|
|
933
|
+
---
|
|
934
|
+
|
|
935
|
+
## Experiments
|
|
936
|
+
|
|
937
|
+
A/B testing with deterministic user bucketing and event tracking.
|
|
938
|
+
|
|
939
|
+
```typescript
|
|
940
|
+
// shared/experiments.ts
|
|
941
|
+
import type { Experiment } from 'ugly-app/shared';
|
|
942
|
+
|
|
943
|
+
export const experiments: Experiment[] = [
|
|
944
|
+
{
|
|
945
|
+
id: 'new-onboarding',
|
|
946
|
+
name: 'New Onboarding Flow',
|
|
947
|
+
description: 'Test the redesigned onboarding',
|
|
948
|
+
active: true,
|
|
949
|
+
branches: [
|
|
950
|
+
{ id: 'control', name: 'Control', weight: 50 },
|
|
951
|
+
{ id: 'variant', name: 'Variant', weight: 50 },
|
|
952
|
+
],
|
|
953
|
+
events: ['ONBOARDING_COMPLETE', 'ONBOARDING_SKIP'],
|
|
954
|
+
},
|
|
955
|
+
];
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
**Server-side assignment:**
|
|
959
|
+
|
|
960
|
+
```typescript
|
|
961
|
+
import { getExperimentAssignments, getExperimentBranch } from 'ugly-app';
|
|
962
|
+
|
|
963
|
+
// Get all active experiment assignments for a user
|
|
964
|
+
const branches = getExperimentAssignments(userId, sessionId, experiments);
|
|
965
|
+
// => { 'new-onboarding': 'variant' }
|
|
966
|
+
|
|
967
|
+
// Get a single experiment branch
|
|
968
|
+
const branch = getExperimentBranch(experiment, userId, sessionId);
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
Bucketing uses a deterministic hash of `experimentId:userId` (or `sessionId` if no user), so the same user always gets the same branch. Weights control the relative distribution across branches.
|
|
972
|
+
|
|
973
|
+
---
|
|
974
|
+
|
|
975
|
+
## Event logging
|
|
976
|
+
|
|
977
|
+
Server-side event capture for analytics, tied to experiments.
|
|
978
|
+
|
|
979
|
+
```typescript
|
|
980
|
+
import { eventLogCapture, eventLogServerCapture } from 'ugly-app';
|
|
981
|
+
|
|
982
|
+
// Capture with returned eventId
|
|
983
|
+
const { eventId } = await eventLogCapture({
|
|
984
|
+
eventName: 'BUTTON_CLICK',
|
|
985
|
+
sessionId,
|
|
986
|
+
userId,
|
|
987
|
+
properties: { page: 'home' },
|
|
988
|
+
experimentBranches: branches,
|
|
989
|
+
}, userId);
|
|
990
|
+
|
|
991
|
+
// Fire-and-forget server capture
|
|
992
|
+
await eventLogServerCapture('SESSION_START', { source: 'web' }, sessionId, userId, branches);
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
**Query functions:**
|
|
996
|
+
|
|
997
|
+
| Function | Description |
|
|
998
|
+
|----------|-------------|
|
|
999
|
+
| `eventLogGetList(input)` | Paginated event list (cursor, date range, filters) |
|
|
1000
|
+
| `eventLogGetTopUsers(input)` | Top users by event count |
|
|
1001
|
+
| `eventLogGetTopSessions(input)` | Top sessions by event count |
|
|
1002
|
+
| `eventLogGetTopEvents(input)` | Top events by frequency |
|
|
1003
|
+
| `eventLogGetCounts(input)` | Time-series counts (granularity: `'seconds'` \| `'minutes'` \| `'days'`) |
|
|
1004
|
+
| `eventLogGetUniqueUsersCounts(input)` | Unique users per time interval |
|
|
1005
|
+
| `eventLogGetUniqueSessionsCounts(input)` | Unique sessions per time interval |
|
|
1006
|
+
|
|
1007
|
+
---
|
|
1008
|
+
|
|
780
1009
|
## Additional server APIs
|
|
781
1010
|
|
|
782
1011
|
### Email
|
|
@@ -816,12 +1045,31 @@ const redis = getRedisClient();
|
|
|
816
1045
|
### Worker queues
|
|
817
1046
|
|
|
818
1047
|
```typescript
|
|
819
|
-
import { createWorkerQueue
|
|
1048
|
+
import { createWorkerQueue } from 'ugly-app';
|
|
1049
|
+
|
|
1050
|
+
const queue = createWorkerQueue({
|
|
1051
|
+
streamName: 'JOBS', // NATS stream name (default: 'JOBS')
|
|
1052
|
+
concurrency: 10, // max concurrent jobs (default: 10)
|
|
1053
|
+
maxRetries: 3, // max retry attempts (default: 3)
|
|
1054
|
+
clockServerOnly: true, // only process if IS_CLOCK_SERVER=true (default: true)
|
|
1055
|
+
});
|
|
820
1056
|
|
|
821
|
-
|
|
822
|
-
|
|
1057
|
+
queue.registerHandler<MyPayload>('sendEmail', async (job) => {
|
|
1058
|
+
job.working(); // extend ack deadline for long-running jobs
|
|
1059
|
+
await doWork(job.payload);
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
await queue.enqueue('sendEmail', { to: 'user@example.com' }, { delay: 5000 });
|
|
1063
|
+
|
|
1064
|
+
configurator.setWorkerQueue(queue); // register with the app for lifecycle management
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
**Scheduled tasks** — prevent duplicate enqueuing via MongoDB upsert:
|
|
1068
|
+
|
|
1069
|
+
```typescript
|
|
1070
|
+
import { enqueueTask } from 'ugly-app';
|
|
823
1071
|
|
|
824
|
-
await enqueueTask(
|
|
1072
|
+
await enqueueTask(taskDoc, queue, { delay: 60000 });
|
|
825
1073
|
```
|
|
826
1074
|
|
|
827
1075
|
### Billing
|
|
@@ -938,9 +1186,9 @@ Run with `npm run db:migrate`. Use `npm run db:migrate -- --status` to preview p
|
|
|
938
1186
|
|
|
939
1187
|
| Import path | Description |
|
|
940
1188
|
|-------------|-------------|
|
|
941
|
-
| `ugly-app` | Server APIs (createApp, DB, auth, AI, email, storage, etc.) |
|
|
942
|
-
| `ugly-app/shared` | Shared types and utilities (defineRequests, defineCollections, definePage, Zod, etc.) |
|
|
943
|
-
| `ugly-app/client` | Client APIs (createSocket, createRouter, AppProvider, components, animations, etc.) |
|
|
1189
|
+
| `ugly-app` | Server APIs (createApp, DB, auth, AI, email, storage, billing, worker queues, etc.) |
|
|
1190
|
+
| `ugly-app/shared` | Shared types and utilities (defineRequests, defineCollections, definePage, experiments, Zod, etc.) |
|
|
1191
|
+
| `ugly-app/client` | Client APIs (createSocket, createRouter, AppProvider, components, animations, audio, etc.) |
|
|
944
1192
|
| `ugly-app/playwright` | Playwright test utilities |
|
|
945
1193
|
| `ugly-app/webrtc` | WebRTC utilities |
|
|
946
1194
|
|
|
@@ -958,6 +1206,8 @@ Run with `npm run db:migrate`. Use `npm run db:migrate -- --status` to preview p
|
|
|
958
1206
|
| `UGLY_BOT_TOKEN` | App token for AI proxy (`/ai/request`) |
|
|
959
1207
|
| `REDIS_URL` | Redis (optional, in-memory fallback for dev) |
|
|
960
1208
|
| `NATS_URL` | NATS server URL |
|
|
1209
|
+
| `CLOCK_ENABLED` | Set to `true` to enable `setOnMinuteTick`/`setOnHourlyTick` handlers |
|
|
1210
|
+
| `IS_CLOCK_SERVER` | Set to `true` on the instance that should process delayed worker queue jobs |
|
|
961
1211
|
| `STORAGE_ACCOUNT_ID` | Cloudflare R2 account ID |
|
|
962
1212
|
| `STORAGE_ACCESS_KEY_ID` | R2 access key |
|
|
963
1213
|
| `STORAGE_SECRET_ACCESS_KEY` | R2 secret key |
|
|
@@ -968,12 +1218,16 @@ Run with `npm run db:migrate`. Use `npm run db:migrate -- --status` to preview p
|
|
|
968
1218
|
| `ANTHROPIC_API_KEY` | Anthropic Claude key |
|
|
969
1219
|
| `OPENAI_API_KEY` | OpenAI key |
|
|
970
1220
|
| `GOOGLE_API_KEY` | Google Gemini key |
|
|
971
|
-
| `
|
|
972
|
-
| `MAILGUN_DOMAIN` | Mailgun sending domain |
|
|
1221
|
+
| `GROQ_API_KEY` | Groq key |
|
|
973
1222
|
| `KIE_API_KEY` | Kie.ai key |
|
|
974
1223
|
| `KIE_BASE_URL` | Kie.ai base URL override (optional) |
|
|
1224
|
+
| `DEEPGRAM_API_KEY` | Deepgram STT key |
|
|
1225
|
+
| `AZURE_TTS_KEY` | Azure TTS key |
|
|
1226
|
+
| `AZURE_TTS_REGION` | Azure TTS region |
|
|
1227
|
+
| `KAGI_API_KEY` | Kagi web search key |
|
|
1228
|
+
| `MAILGUN_API_KEY` | Mailgun key |
|
|
1229
|
+
| `MAILGUN_DOMAIN` | Mailgun sending domain |
|
|
975
1230
|
| `MAILGUN_FROM` | Default from address |
|
|
976
|
-
| `CLOCK_ENABLED` | Set to `true` to enable `setOnMinuteTick`/`setOnHourlyTick` handlers |
|
|
977
1231
|
|
|
978
1232
|
Client-side variables must be prefixed with `VITE_`.
|
|
979
1233
|
|
|
@@ -787,7 +787,7 @@ function PopupLayer({ popups }: { popups: PopupEntry[] }) {
|
|
|
787
787
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
788
788
|
Code coverage generated by
|
|
789
789
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
790
|
-
at 2026-03-
|
|
790
|
+
at 2026-03-30T16:02:56.403Z
|
|
791
791
|
</div>
|
|
792
792
|
<script src="../prettify.js"></script>
|
|
793
793
|
<script>
|
|
@@ -118,7 +118,7 @@ export function clearFeedbackContext(): void {
|
|
|
118
118
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
119
119
|
Code coverage generated by
|
|
120
120
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
121
|
-
at 2026-03-
|
|
121
|
+
at 2026-03-30T16:02:56.403Z
|
|
122
122
|
</div>
|
|
123
123
|
<script src="../prettify.js"></script>
|
|
124
124
|
<script>
|
|
@@ -310,7 +310,7 @@ export function LoginPopup({ onSuccess, inline = false }: LoginPopupProps) {
|
|
|
310
310
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
311
311
|
Code coverage generated by
|
|
312
312
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
313
|
-
at 2026-03-
|
|
313
|
+
at 2026-03-30T16:02:56.403Z
|
|
314
314
|
</div>
|
|
315
315
|
<script src="../prettify.js"></script>
|
|
316
316
|
<script>
|
|
@@ -1918,7 +1918,7 @@ export function Link<
|
|
|
1918
1918
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
1919
1919
|
Code coverage generated by
|
|
1920
1920
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
1921
|
-
at 2026-03-
|
|
1921
|
+
at 2026-03-30T16:02:56.403Z
|
|
1922
1922
|
</div>
|
|
1923
1923
|
<script src="../prettify.js"></script>
|
|
1924
1924
|
<script>
|
|
@@ -244,7 +244,7 @@ function blobToDataUrl(blob: Blob): Promise<string> {
|
|
|
244
244
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
245
245
|
Code coverage generated by
|
|
246
246
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
247
|
-
at 2026-03-
|
|
247
|
+
at 2026-03-30T16:02:56.403Z
|
|
248
248
|
</div>
|
|
249
249
|
<script src="../prettify.js"></script>
|
|
250
250
|
<script>
|
|
@@ -763,7 +763,7 @@ export function ViewFlipper({
|
|
|
763
763
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
764
764
|
Code coverage generated by
|
|
765
765
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
766
|
-
at 2026-03-
|
|
766
|
+
at 2026-03-30T16:02:56.403Z
|
|
767
767
|
</div>
|
|
768
768
|
<script src="../prettify.js"></script>
|
|
769
769
|
<script>
|
|
@@ -424,7 +424,7 @@ export const Animated = {
|
|
|
424
424
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
425
425
|
Code coverage generated by
|
|
426
426
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
427
|
-
at 2026-03-
|
|
427
|
+
at 2026-03-30T16:02:56.403Z
|
|
428
428
|
</div>
|
|
429
429
|
<script src="../../prettify.js"></script>
|
|
430
430
|
<script>
|
|
@@ -652,7 +652,7 @@ export function useAnimatedValueTracker(animatedValue: AnimatedValueRef): number
|
|
|
652
652
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
653
653
|
Code coverage generated by
|
|
654
654
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
655
|
-
at 2026-03-
|
|
655
|
+
at 2026-03-30T16:02:56.403Z
|
|
656
656
|
</div>
|
|
657
657
|
<script src="../../prettify.js"></script>
|
|
658
658
|
<script>
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
117
117
|
Code coverage generated by
|
|
118
118
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
119
|
-
at 2026-03-
|
|
119
|
+
at 2026-03-30T16:02:56.403Z
|
|
120
120
|
</div>
|
|
121
121
|
<script src="../../prettify.js"></script>
|
|
122
122
|
<script>
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
117
117
|
Code coverage generated by
|
|
118
118
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
119
|
-
at 2026-03-
|
|
119
|
+
at 2026-03-30T16:02:56.403Z
|
|
120
120
|
</div>
|
|
121
121
|
<script src="../../prettify.js"></script>
|
|
122
122
|
<script>
|
|
@@ -269,7 +269,7 @@
|
|
|
269
269
|
<span class="cline-any cline-yes">19x</span>
|
|
270
270
|
<span class="cline-any cline-yes">18x</span>
|
|
271
271
|
<span class="cline-any cline-yes">18x</span>
|
|
272
|
-
<span class="cline-any cline-yes">
|
|
272
|
+
<span class="cline-any cline-yes">265x</span>
|
|
273
273
|
<span class="cline-any cline-yes">4x</span>
|
|
274
274
|
<span class="cline-any cline-yes">4x</span>
|
|
275
275
|
<span class="cline-any cline-neutral"> </span>
|
|
@@ -433,7 +433,7 @@ export function useSTT(socket: SocketLike, options: STTOptions = {}) {
|
|
|
433
433
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
434
434
|
Code coverage generated by
|
|
435
435
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
436
|
-
at 2026-03-
|
|
436
|
+
at 2026-03-30T16:02:56.403Z
|
|
437
437
|
</div>
|
|
438
438
|
<script src="../../prettify.js"></script>
|
|
439
439
|
<script>
|
|
@@ -367,7 +367,7 @@ export function useTTS(socket: SocketLike) {
|
|
|
367
367
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
368
368
|
Code coverage generated by
|
|
369
369
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
370
|
-
at 2026-03-
|
|
370
|
+
at 2026-03-30T16:02:56.403Z
|
|
371
371
|
</div>
|
|
372
372
|
<script src="../../prettify.js"></script>
|
|
373
373
|
<script>
|
|
@@ -346,7 +346,7 @@ export function Button({
|
|
|
346
346
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
347
347
|
Code coverage generated by
|
|
348
348
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
349
|
-
at 2026-03-
|
|
349
|
+
at 2026-03-30T16:02:56.403Z
|
|
350
350
|
</div>
|
|
351
351
|
<script src="../../prettify.js"></script>
|
|
352
352
|
<script>
|
|
@@ -604,7 +604,7 @@ export function FeedbackButton({ socket }: Props): ReactNode {
|
|
|
604
604
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
605
605
|
Code coverage generated by
|
|
606
606
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
607
|
-
at 2026-03-
|
|
607
|
+
at 2026-03-30T16:02:56.403Z
|
|
608
608
|
</div>
|
|
609
609
|
<script src="../../prettify.js"></script>
|
|
610
610
|
<script>
|
|
@@ -262,7 +262,7 @@ export function Input({
|
|
|
262
262
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
263
263
|
Code coverage generated by
|
|
264
264
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
265
|
-
at 2026-03-
|
|
265
|
+
at 2026-03-30T16:02:56.403Z
|
|
266
266
|
</div>
|
|
267
267
|
<script src="../../prettify.js"></script>
|
|
268
268
|
<script>
|
|
@@ -244,7 +244,7 @@ export function Modal({
|
|
|
244
244
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
245
245
|
Code coverage generated by
|
|
246
246
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
247
|
-
at 2026-03-
|
|
247
|
+
at 2026-03-30T16:02:56.403Z
|
|
248
248
|
</div>
|
|
249
249
|
<script src="../../prettify.js"></script>
|
|
250
250
|
<script>
|
|
@@ -334,7 +334,7 @@ export function Text({
|
|
|
334
334
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
335
335
|
Code coverage generated by
|
|
336
336
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
337
|
-
at 2026-03-
|
|
337
|
+
at 2026-03-30T16:02:56.403Z
|
|
338
338
|
</div>
|
|
339
339
|
<script src="../../prettify.js"></script>
|
|
340
340
|
<script>
|
|
@@ -220,7 +220,7 @@ export function Toast({
|
|
|
220
220
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
221
221
|
Code coverage generated by
|
|
222
222
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
223
|
-
at 2026-03-
|
|
223
|
+
at 2026-03-30T16:02:56.403Z
|
|
224
224
|
</div>
|
|
225
225
|
<script src="../../prettify.js"></script>
|
|
226
226
|
<script>
|
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
192
192
|
Code coverage generated by
|
|
193
193
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
194
|
-
at 2026-03-
|
|
194
|
+
at 2026-03-30T16:02:56.403Z
|
|
195
195
|
</div>
|
|
196
196
|
<script src="../../prettify.js"></script>
|
|
197
197
|
<script>
|
|
@@ -103,7 +103,7 @@ export const Z = {
|
|
|
103
103
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
104
104
|
Code coverage generated by
|
|
105
105
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
106
|
-
at 2026-03-
|
|
106
|
+
at 2026-03-30T16:02:56.403Z
|
|
107
107
|
</div>
|
|
108
108
|
<script src="../../prettify.js"></script>
|
|
109
109
|
<script>
|