@comergehq/studio 0.1.2 → 0.1.4
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/dist/index.d.mts +2 -10
- package/dist/index.d.ts +2 -10
- package/dist/index.js +293 -264
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +251 -222
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -5
- package/src/components/chat/ChatComposer.tsx +277 -0
- package/src/components/chat/ChatHeader.tsx +31 -0
- package/src/components/chat/ChatMessageBubble.tsx +69 -0
- package/src/components/chat/ChatMessageList.tsx +137 -0
- package/src/components/chat/ChatPage.tsx +69 -0
- package/src/components/chat/ForkNoticeBanner.tsx +66 -0
- package/src/components/chat/MultilineTextInput.tsx +46 -0
- package/src/components/chat/ScrollToBottomButton.tsx +78 -0
- package/src/components/chat/TypingIndicator.tsx +54 -0
- package/src/components/chat/index.ts +28 -0
- package/src/components/comments/AppCommentsSheet.tsx +213 -0
- package/src/components/comments/CommentRow.tsx +63 -0
- package/src/components/comments/formatTimeAgo.ts +3 -0
- package/src/components/comments/index.ts +3 -0
- package/src/components/comments/useAppComments.ts +74 -0
- package/src/components/comments/useAppDetails.ts +35 -0
- package/src/components/comments/useIosKeyboardSnapFix.ts +24 -0
- package/src/components/dialogs/ConfirmMergeRequestDialog.tsx +156 -0
- package/src/components/dialogs/index.ts +4 -0
- package/src/components/draw/DrawColorPicker.tsx +77 -0
- package/src/components/draw/DrawModeOverlay.tsx +144 -0
- package/src/components/draw/DrawSurface.tsx +127 -0
- package/src/components/draw/DrawToolbar.tsx +253 -0
- package/src/components/draw/index.ts +15 -0
- package/src/components/draw/optionalHaptics.ts +15 -0
- package/src/components/draw/strokes.ts +21 -0
- package/src/components/draw/types.ts +9 -0
- package/src/components/floating-draggable-button/FloatingDraggableButton.tsx +323 -0
- package/src/components/floating-draggable-button/constants.ts +17 -0
- package/src/components/floating-draggable-button/index.ts +4 -0
- package/src/components/floating-draggable-button/types.ts +63 -0
- package/src/components/icons/MergeIcon.tsx +14 -0
- package/src/components/icons/StudioIcons.tsx +66 -0
- package/src/components/index.ts +17 -0
- package/src/components/merge-requests/MergeRequestStatusCard.tsx +179 -0
- package/src/components/merge-requests/ReviewMergeRequestActionButton.tsx +62 -0
- package/src/components/merge-requests/ReviewMergeRequestCard.tsx +192 -0
- package/src/components/merge-requests/ReviewMergeRequestCarousel.tsx +132 -0
- package/src/components/merge-requests/index.ts +7 -0
- package/src/components/merge-requests/mergeRequestStatusDisplay.ts +23 -0
- package/src/components/merge-requests/toIsoString.ts +9 -0
- package/src/components/merge-requests/useControlledExpansion.ts +16 -0
- package/src/components/models/index.ts +9 -0
- package/src/components/models/types.ts +43 -0
- package/src/components/overlays/EdgeGlowFrame.tsx +105 -0
- package/src/components/overlays/index.ts +4 -0
- package/src/components/preview/PreviewHeroCard.tsx +58 -0
- package/src/components/preview/PreviewImage.tsx +22 -0
- package/src/components/preview/PreviewMetaRow.tsx +70 -0
- package/src/components/preview/PreviewPage.tsx +36 -0
- package/src/components/preview/PreviewPlaceholder.tsx +72 -0
- package/src/components/preview/PreviewStatusBadge.tsx +63 -0
- package/src/components/preview/StatsBar.tsx +109 -0
- package/src/components/preview/index.ts +22 -0
- package/src/components/primitives/Avatar.tsx +68 -0
- package/src/components/primitives/Button.tsx +102 -0
- package/src/components/primitives/Card.tsx +30 -0
- package/src/components/primitives/Divider.tsx +17 -0
- package/src/components/primitives/Icon.tsx +40 -0
- package/src/components/primitives/MarkdownText.tsx +72 -0
- package/src/components/primitives/Modal.tsx +53 -0
- package/src/components/primitives/Surface.tsx +42 -0
- package/src/components/primitives/Text.tsx +83 -0
- package/src/components/primitives/index.ts +35 -0
- package/src/components/primitives/types.ts +30 -0
- package/src/components/studio-sheet/StudioBottomSheet.tsx +114 -0
- package/src/components/studio-sheet/StudioSheetBackground.tsx +63 -0
- package/src/components/studio-sheet/StudioSheetHeader.tsx +35 -0
- package/src/components/studio-sheet/StudioSheetHeaderIconButton.tsx +109 -0
- package/src/components/studio-sheet/StudioSheetPager.tsx +66 -0
- package/src/components/studio-sheet/index.ts +18 -0
- package/src/components/studio-sheet/types.ts +5 -0
- package/src/components/utils/color.ts +25 -0
- package/src/components/utils/formatTimeAgo.ts +19 -0
- package/src/core/logger.ts +42 -0
- package/src/core/services/http/baseUrl.ts +3 -0
- package/src/core/services/http/index.ts +128 -0
- package/src/core/services/http/public.ts +33 -0
- package/src/core/services/supabase/auth.ts +41 -0
- package/src/core/services/supabase/client.ts +43 -0
- package/src/core/services/supabase/index.ts +7 -0
- package/src/data/agent/remote.ts +30 -0
- package/src/data/agent/repository.ts +34 -0
- package/src/data/agent/types.ts +28 -0
- package/src/data/apps/bundles/remote.ts +47 -0
- package/src/data/apps/bundles/repository.ts +35 -0
- package/src/data/apps/bundles/types.ts +27 -0
- package/src/data/apps/images/remote.ts +61 -0
- package/src/data/apps/images/repository.ts +47 -0
- package/src/data/apps/remote.ts +97 -0
- package/src/data/apps/repository.ts +185 -0
- package/src/data/apps/types.ts +206 -0
- package/src/data/attachment/remote.ts +32 -0
- package/src/data/attachment/repository.ts +40 -0
- package/src/data/attachment/types.ts +42 -0
- package/src/data/base-remote.ts +3 -0
- package/src/data/base-repository.ts +11 -0
- package/src/data/comments/likes/remote.ts +87 -0
- package/src/data/comments/likes/repository.ts +61 -0
- package/src/data/comments/likes/types.ts +47 -0
- package/src/data/comments/remote.ts +71 -0
- package/src/data/comments/repository.ts +53 -0
- package/src/data/comments/types.ts +60 -0
- package/src/data/github/remote.ts +23 -0
- package/src/data/github/repository.ts +35 -0
- package/src/data/github/types.ts +23 -0
- package/src/data/home/remote.ts +24 -0
- package/src/data/home/repository.ts +28 -0
- package/src/data/home/types.ts +70 -0
- package/src/data/index.ts +3 -0
- package/src/data/likes/remote.ts +57 -0
- package/src/data/likes/repository.ts +47 -0
- package/src/data/likes/types.ts +46 -0
- package/src/data/me/remote.ts +28 -0
- package/src/data/me/repository.ts +30 -0
- package/src/data/me/types.ts +14 -0
- package/src/data/merge-requests/remote.ts +76 -0
- package/src/data/merge-requests/repository.ts +66 -0
- package/src/data/merge-requests/types.ts +33 -0
- package/src/data/messages/remote.ts +21 -0
- package/src/data/messages/repository.ts +104 -0
- package/src/data/messages/types.ts +20 -0
- package/src/data/public/studio-config/remote.ts +19 -0
- package/src/data/public/studio-config/repository.ts +23 -0
- package/src/data/public/studio-config/types.ts +6 -0
- package/src/data/ratings/remote.ts +76 -0
- package/src/data/ratings/repository.ts +63 -0
- package/src/data/ratings/types.ts +57 -0
- package/src/data/threads/remote.ts +40 -0
- package/src/data/threads/repository.ts +41 -0
- package/src/data/threads/types.ts +25 -0
- package/src/data/types.ts +8 -0
- package/src/data/users/remote.ts +31 -0
- package/src/data/users/repository.ts +45 -0
- package/src/data/users/types.ts +15 -0
- package/src/index.ts +6 -0
- package/src/studio/ComergeStudio.tsx +239 -0
- package/src/studio/bootstrap/StudioBootstrap.tsx +45 -0
- package/src/studio/bootstrap/useStudioBootstrap.ts +55 -0
- package/src/studio/hooks/useApp.ts +83 -0
- package/src/studio/hooks/useAppStats.ts +111 -0
- package/src/studio/hooks/useAttachmentUpload.ts +59 -0
- package/src/studio/hooks/useBundleManager.ts +389 -0
- package/src/studio/hooks/useMergeRequests.ts +173 -0
- package/src/studio/hooks/useStudioActions.ts +96 -0
- package/src/studio/hooks/useThreadMessages.ts +85 -0
- package/src/studio/lib/chat.ts +34 -0
- package/src/studio/ui/ChatPanel.tsx +154 -0
- package/src/studio/ui/ConfirmMergeFlow.tsx +55 -0
- package/src/studio/ui/PreviewPanel.tsx +131 -0
- package/src/studio/ui/RuntimeRenderer.tsx +40 -0
- package/src/studio/ui/StudioOverlay.tsx +257 -0
- package/src/studio/ui/preview-panel/PressableCardRow.tsx +49 -0
- package/src/studio/ui/preview-panel/PreviewCollaborateSection.tsx +174 -0
- package/src/studio/ui/preview-panel/PreviewCustomizeSection.tsx +160 -0
- package/src/studio/ui/preview-panel/PreviewHeroSection.tsx +56 -0
- package/src/studio/ui/preview-panel/PreviewMetaSection.tsx +67 -0
- package/src/studio/ui/preview-panel/PreviewPanelHeader.tsx +48 -0
- package/src/studio/ui/preview-panel/SectionTitle.tsx +31 -0
- package/src/studio/ui/preview-panel/usePreviewPanelData.ts +132 -0
- package/src/studio/ui/preview-panel/utils.ts +29 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/tokens.ts +118 -0
- package/src/theme/types.ts +90 -0
- package/src/theme/useTheme.ts +11 -0
- package/dist/assets/images/merge.svg +0 -3
- package/dist/merge-72UG27QV.svg +0 -3
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { api } from '../../core/services/http';
|
|
2
|
+
import type { ServiceResponse } from '../types';
|
|
3
|
+
import { BaseRemote } from '../base-remote';
|
|
4
|
+
import type { UserStats, UserStatsBatchResponse } from './types';
|
|
5
|
+
|
|
6
|
+
export interface UsersRemoteDataSource {
|
|
7
|
+
getStats(userId: string): Promise<ServiceResponse<UserStats>>;
|
|
8
|
+
getStatsBatch(userIds: string[]): Promise<ServiceResponse<UserStatsBatchResponse>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class UsersRemoteDataSourceImpl extends BaseRemote implements UsersRemoteDataSource {
|
|
12
|
+
async getStats(userId: string): Promise<ServiceResponse<UserStats>> {
|
|
13
|
+
const { data } = await api.get<ServiceResponse<UserStats>>(
|
|
14
|
+
`/v1/users/${encodeURIComponent(userId)}/stats`
|
|
15
|
+
);
|
|
16
|
+
return data;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getStatsBatch(userIds: string[]): Promise<ServiceResponse<UserStatsBatchResponse>> {
|
|
20
|
+
const { data } = await api.post<ServiceResponse<UserStatsBatchResponse>>(
|
|
21
|
+
'/v1/users/stats/batch',
|
|
22
|
+
{ userIds }
|
|
23
|
+
);
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const usersRemoteDataSource: UsersRemoteDataSource = new UsersRemoteDataSourceImpl();
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { UsersRemoteDataSource } from './remote';
|
|
2
|
+
import { usersRemoteDataSource } from './remote';
|
|
3
|
+
import type { UserStats, UserStatsBatchResponse } from './types';
|
|
4
|
+
import { BaseRepository } from '../../data/base-repository';
|
|
5
|
+
|
|
6
|
+
export interface UsersRepository {
|
|
7
|
+
getStats(userId: string): Promise<UserStats>;
|
|
8
|
+
getStatsBatch(userIds: string[]): Promise<Record<string, UserStats>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class UsersRepositoryImpl extends BaseRepository implements UsersRepository {
|
|
12
|
+
constructor(private readonly remote: UsersRemoteDataSource) {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async getStats(userId: string): Promise<UserStats> {
|
|
17
|
+
const res = await this.remote.getStats(userId);
|
|
18
|
+
return this.unwrapOrThrow(res);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async getStatsBatch(userIds: string[]): Promise<Record<string, UserStats>> {
|
|
22
|
+
if (userIds.length === 0) return {};
|
|
23
|
+
const res = await this.remote.getStatsBatch(userIds);
|
|
24
|
+
if (res.responseObject && !res.success) {
|
|
25
|
+
return this.extractStats(res.responseObject);
|
|
26
|
+
}
|
|
27
|
+
const payload = this.unwrapOrThrow(res);
|
|
28
|
+
return this.extractStats(payload);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private extractStats(payload: UserStatsBatchResponse): Record<string, UserStats> {
|
|
32
|
+
const result: Record<string, UserStats> = {};
|
|
33
|
+
Object.entries(payload.stats).forEach(([userId, stats]) => {
|
|
34
|
+
if (stats) {
|
|
35
|
+
result[userId] = stats;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const usersRepository: UsersRepository = new UsersRepositoryImpl(usersRemoteDataSource);
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type UserStats = {
|
|
2
|
+
userId: string;
|
|
3
|
+
name: string | null;
|
|
4
|
+
avatar: string | null;
|
|
5
|
+
approvedOpenedMergeRequests: number;
|
|
6
|
+
totalOpenedMergeRequests: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type UserStatsBatchResponse = {
|
|
10
|
+
stats: Record<string, UserStats | null>;
|
|
11
|
+
errors?: Record<string, { message: string; statusCode: number }>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Platform as RNPlatform, View, type ViewStyle } from 'react-native';
|
|
3
|
+
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
|
4
|
+
|
|
5
|
+
import type { Platform as BundlePlatform } from '../data/apps/bundles/types';
|
|
6
|
+
import { StudioBootstrap } from './bootstrap/StudioBootstrap';
|
|
7
|
+
import { useApp } from './hooks/useApp';
|
|
8
|
+
import { useThreadMessages } from './hooks/useThreadMessages';
|
|
9
|
+
import { useBundleManager } from './hooks/useBundleManager';
|
|
10
|
+
import { useMergeRequests } from './hooks/useMergeRequests';
|
|
11
|
+
import { useAttachmentUpload } from './hooks/useAttachmentUpload';
|
|
12
|
+
import { useStudioActions } from './hooks/useStudioActions';
|
|
13
|
+
import { hasNoOutcomeAfterLastHuman } from './lib/chat';
|
|
14
|
+
import { RuntimeRenderer } from './ui/RuntimeRenderer';
|
|
15
|
+
import { StudioOverlay } from './ui/StudioOverlay';
|
|
16
|
+
|
|
17
|
+
export type ComergeStudioProps = {
|
|
18
|
+
appId: string;
|
|
19
|
+
apiKey: string;
|
|
20
|
+
appKey?: string;
|
|
21
|
+
onNavigateHome?: () => void;
|
|
22
|
+
style?: ViewStyle;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function ComergeStudio({
|
|
26
|
+
appId,
|
|
27
|
+
apiKey,
|
|
28
|
+
appKey = 'MicroMain',
|
|
29
|
+
onNavigateHome,
|
|
30
|
+
style,
|
|
31
|
+
}: ComergeStudioProps) {
|
|
32
|
+
const [activeAppId, setActiveAppId] = React.useState(appId);
|
|
33
|
+
const [runtimeAppId, setRuntimeAppId] = React.useState(appId);
|
|
34
|
+
const [pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId] = React.useState<string | null>(null);
|
|
35
|
+
const platform = React.useMemo<BundlePlatform>(() => (RNPlatform.OS === 'ios' ? 'ios' : 'android'), []);
|
|
36
|
+
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
setActiveAppId(appId);
|
|
39
|
+
setRuntimeAppId(appId);
|
|
40
|
+
setPendingRuntimeTargetAppId(null);
|
|
41
|
+
}, [appId]);
|
|
42
|
+
|
|
43
|
+
const captureTargetRef = React.useRef<View | null>(null);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<StudioBootstrap apiKey={apiKey}>
|
|
47
|
+
{({ userId }) => (
|
|
48
|
+
<BottomSheetModalProvider>
|
|
49
|
+
<ComergeStudioInner
|
|
50
|
+
userId={userId}
|
|
51
|
+
activeAppId={activeAppId}
|
|
52
|
+
setActiveAppId={setActiveAppId}
|
|
53
|
+
runtimeAppId={runtimeAppId}
|
|
54
|
+
setRuntimeAppId={setRuntimeAppId}
|
|
55
|
+
pendingRuntimeTargetAppId={pendingRuntimeTargetAppId}
|
|
56
|
+
setPendingRuntimeTargetAppId={setPendingRuntimeTargetAppId}
|
|
57
|
+
appKey={appKey}
|
|
58
|
+
platform={platform}
|
|
59
|
+
onNavigateHome={onNavigateHome}
|
|
60
|
+
captureTargetRef={captureTargetRef}
|
|
61
|
+
style={style}
|
|
62
|
+
/>
|
|
63
|
+
</BottomSheetModalProvider>
|
|
64
|
+
)}
|
|
65
|
+
</StudioBootstrap>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type InnerProps = {
|
|
70
|
+
userId: string;
|
|
71
|
+
activeAppId: string;
|
|
72
|
+
setActiveAppId: (id: string) => void;
|
|
73
|
+
runtimeAppId: string;
|
|
74
|
+
setRuntimeAppId: (id: string) => void;
|
|
75
|
+
pendingRuntimeTargetAppId: string | null;
|
|
76
|
+
setPendingRuntimeTargetAppId: (id: string | null) => void;
|
|
77
|
+
appKey: string;
|
|
78
|
+
platform: BundlePlatform;
|
|
79
|
+
onNavigateHome?: () => void;
|
|
80
|
+
captureTargetRef: React.RefObject<View | null>;
|
|
81
|
+
style?: ViewStyle;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function ComergeStudioInner({
|
|
85
|
+
userId,
|
|
86
|
+
activeAppId,
|
|
87
|
+
setActiveAppId,
|
|
88
|
+
runtimeAppId,
|
|
89
|
+
setRuntimeAppId,
|
|
90
|
+
pendingRuntimeTargetAppId,
|
|
91
|
+
setPendingRuntimeTargetAppId,
|
|
92
|
+
appKey,
|
|
93
|
+
platform,
|
|
94
|
+
onNavigateHome,
|
|
95
|
+
captureTargetRef,
|
|
96
|
+
style,
|
|
97
|
+
}: InnerProps) {
|
|
98
|
+
const { app, loading: appLoading } = useApp(activeAppId);
|
|
99
|
+
const { app: runtimeAppFromHook } = useApp(runtimeAppId, { enabled: runtimeAppId !== activeAppId });
|
|
100
|
+
const runtimeApp = runtimeAppId === activeAppId ? app : runtimeAppFromHook;
|
|
101
|
+
|
|
102
|
+
// When we fork+edit, we keep rendering the original app until the forked app completes the edit.
|
|
103
|
+
// We unlock the runtime switch once we observe the forked app go editing -> ready.
|
|
104
|
+
const sawEditingOnPendingTargetRef = React.useRef(false);
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
sawEditingOnPendingTargetRef.current = false;
|
|
107
|
+
}, [pendingRuntimeTargetAppId]);
|
|
108
|
+
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
if (!pendingRuntimeTargetAppId) return;
|
|
111
|
+
if (activeAppId !== pendingRuntimeTargetAppId) return;
|
|
112
|
+
if (app?.status === 'editing') {
|
|
113
|
+
sawEditingOnPendingTargetRef.current = true;
|
|
114
|
+
}
|
|
115
|
+
if (sawEditingOnPendingTargetRef.current && app?.status === 'ready') {
|
|
116
|
+
setRuntimeAppId(pendingRuntimeTargetAppId);
|
|
117
|
+
setPendingRuntimeTargetAppId(null);
|
|
118
|
+
sawEditingOnPendingTargetRef.current = false;
|
|
119
|
+
}
|
|
120
|
+
}, [activeAppId, app?.status, app?.id, pendingRuntimeTargetAppId, setPendingRuntimeTargetAppId, setRuntimeAppId]);
|
|
121
|
+
|
|
122
|
+
const bundle = useBundleManager({
|
|
123
|
+
base: { appId: runtimeAppId, commitId: runtimeApp?.headCommitId ?? undefined },
|
|
124
|
+
platform,
|
|
125
|
+
canRequestLatest: runtimeApp?.status === 'ready',
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const threadId = app?.threadId ?? '';
|
|
129
|
+
const thread = useThreadMessages(threadId);
|
|
130
|
+
|
|
131
|
+
const mergeRequests = useMergeRequests({ appId: activeAppId });
|
|
132
|
+
const hasOpenOutgoingMr = React.useMemo(() => {
|
|
133
|
+
return mergeRequests.lists.outgoing.some((mr) => mr.status === 'open');
|
|
134
|
+
}, [mergeRequests.lists.outgoing]);
|
|
135
|
+
|
|
136
|
+
const incomingReviewMrs = React.useMemo(() => {
|
|
137
|
+
if (!userId) return mergeRequests.lists.incoming;
|
|
138
|
+
return mergeRequests.lists.incoming.filter((mr) => mr.createdBy !== userId);
|
|
139
|
+
}, [mergeRequests.lists.incoming, userId]);
|
|
140
|
+
|
|
141
|
+
const uploader = useAttachmentUpload();
|
|
142
|
+
|
|
143
|
+
const actions = useStudioActions({
|
|
144
|
+
userId,
|
|
145
|
+
app,
|
|
146
|
+
onForkedApp: (id, opts) => {
|
|
147
|
+
setActiveAppId(id);
|
|
148
|
+
const keepRenderingAppId = opts?.keepRenderingAppId;
|
|
149
|
+
if (keepRenderingAppId) {
|
|
150
|
+
setRuntimeAppId(keepRenderingAppId);
|
|
151
|
+
setPendingRuntimeTargetAppId(id);
|
|
152
|
+
} else {
|
|
153
|
+
setRuntimeAppId(id);
|
|
154
|
+
setPendingRuntimeTargetAppId(null);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
uploadAttachments: uploader.uploadBase64Images,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const chatSendDisabled = hasNoOutcomeAfterLastHuman(thread.raw);
|
|
161
|
+
|
|
162
|
+
const [processingMrId, setProcessingMrId] = React.useState<string | null>(null);
|
|
163
|
+
const [testingMrId, setTestingMrId] = React.useState<string | null>(null);
|
|
164
|
+
|
|
165
|
+
// Show typing dots when the last message isn't an outcome (agent still working).
|
|
166
|
+
const chatShowTypingIndicator = React.useMemo(() => {
|
|
167
|
+
if (!thread.raw || thread.raw.length === 0) return false;
|
|
168
|
+
const last = thread.raw[thread.raw.length - 1];
|
|
169
|
+
const payloadType = typeof (last.payload as any)?.type === 'string' ? String((last.payload as any).type) : undefined;
|
|
170
|
+
return payloadType !== 'outcome';
|
|
171
|
+
}, [thread.raw]);
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<View style={[{ flex: 1 }, style]}>
|
|
175
|
+
<View ref={captureTargetRef} style={{ flex: 1 }} collapsable={false}>
|
|
176
|
+
<RuntimeRenderer appKey={appKey} bundlePath={bundle.bundlePath} renderToken={bundle.renderToken} />
|
|
177
|
+
|
|
178
|
+
<StudioOverlay
|
|
179
|
+
captureTargetRef={captureTargetRef}
|
|
180
|
+
app={app}
|
|
181
|
+
appLoading={appLoading}
|
|
182
|
+
isOwner={actions.isOwner}
|
|
183
|
+
shouldForkOnEdit={actions.shouldForkOnEdit}
|
|
184
|
+
isTesting={bundle.isTesting}
|
|
185
|
+
onRestoreBase={async () => {
|
|
186
|
+
setTestingMrId(null);
|
|
187
|
+
await bundle.restoreBase();
|
|
188
|
+
}}
|
|
189
|
+
incomingMergeRequests={incomingReviewMrs}
|
|
190
|
+
outgoingMergeRequests={mergeRequests.lists.outgoing}
|
|
191
|
+
creatorStatsById={mergeRequests.creatorStatsById}
|
|
192
|
+
processingMrId={processingMrId}
|
|
193
|
+
isBuildingMrTest={bundle.loading}
|
|
194
|
+
testingMrId={testingMrId}
|
|
195
|
+
toMergeRequestSummary={mergeRequests.toSummary}
|
|
196
|
+
onSubmitMergeRequest={
|
|
197
|
+
app?.forkedFromAppId && actions.isOwner && !hasOpenOutgoingMr
|
|
198
|
+
? async () => {
|
|
199
|
+
await mergeRequests.actions.openMergeRequest(activeAppId);
|
|
200
|
+
}
|
|
201
|
+
: undefined
|
|
202
|
+
}
|
|
203
|
+
onApprove={async (mr) => {
|
|
204
|
+
if (processingMrId) return;
|
|
205
|
+
setProcessingMrId(mr.id);
|
|
206
|
+
try {
|
|
207
|
+
await mergeRequests.actions.approve(mr.id);
|
|
208
|
+
} finally {
|
|
209
|
+
setProcessingMrId(null);
|
|
210
|
+
}
|
|
211
|
+
}}
|
|
212
|
+
onReject={async (mr) => {
|
|
213
|
+
if (processingMrId) return;
|
|
214
|
+
setProcessingMrId(mr.id);
|
|
215
|
+
try {
|
|
216
|
+
await mergeRequests.actions.reject(mr.id);
|
|
217
|
+
} finally {
|
|
218
|
+
setProcessingMrId(null);
|
|
219
|
+
}
|
|
220
|
+
}}
|
|
221
|
+
onTestMr={async (mr) => {
|
|
222
|
+
setTestingMrId(mr.id);
|
|
223
|
+
await bundle.loadTest({ appId: mr.sourceAppId, commitId: mr.sourceTipCommitId ?? mr.sourceCommitId });
|
|
224
|
+
}}
|
|
225
|
+
chatMessages={thread.messages}
|
|
226
|
+
chatLoading={thread.loading}
|
|
227
|
+
chatSendDisabled={chatSendDisabled}
|
|
228
|
+
chatForking={actions.forking}
|
|
229
|
+
chatSending={actions.sending}
|
|
230
|
+
chatShowTypingIndicator={chatShowTypingIndicator}
|
|
231
|
+
onSendChat={(text, attachments) => actions.sendEdit({ prompt: text, attachments })}
|
|
232
|
+
onNavigateHome={onNavigateHome}
|
|
233
|
+
/>
|
|
234
|
+
</View>
|
|
235
|
+
</View>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { Text } from '../../components/primitives/Text';
|
|
5
|
+
import { useStudioBootstrap, type UseStudioBootstrapOptions } from './useStudioBootstrap';
|
|
6
|
+
|
|
7
|
+
export type StudioBootstrapProps = UseStudioBootstrapOptions & {
|
|
8
|
+
children: React.ReactNode | ((params: { userId: string }) => React.ReactNode);
|
|
9
|
+
/**
|
|
10
|
+
* Optional custom loading UI.
|
|
11
|
+
*/
|
|
12
|
+
fallback?: React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Optional custom error UI. If not provided, a minimal message is shown.
|
|
15
|
+
*/
|
|
16
|
+
renderError?: (error: Error) => React.ReactNode;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function StudioBootstrap({ children, fallback, renderError, apiKey }: StudioBootstrapProps) {
|
|
20
|
+
const { ready, error, userId } = useStudioBootstrap({ apiKey });
|
|
21
|
+
|
|
22
|
+
if (error) {
|
|
23
|
+
return (
|
|
24
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 }}>
|
|
25
|
+
{renderError ? renderError(error) : <Text variant="bodyMuted">{error.message}</Text>}
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!ready) {
|
|
31
|
+
return (
|
|
32
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 }}>
|
|
33
|
+
{fallback ?? <Text variant="bodyMuted">Loading…</Text>}
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof children === 'function') {
|
|
39
|
+
return <>{children({ userId: userId ?? '' })}</>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return <>{children}</>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { setClientApiKey } from '../../core/services/http/public';
|
|
4
|
+
import { ensureAuthenticatedSession, ensureAnonymousSession } from '../../core/services/supabase/auth';
|
|
5
|
+
import { isSupabaseClientInjected, setSupabaseConfig } from '../../core/services/supabase/client';
|
|
6
|
+
import { studioConfigRepository } from '../../data/public/studio-config/repository';
|
|
7
|
+
|
|
8
|
+
export type UseStudioBootstrapOptions = {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type StudioBootstrapState = {
|
|
13
|
+
ready: boolean;
|
|
14
|
+
userId: string | null;
|
|
15
|
+
error: Error | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function useStudioBootstrap(options: UseStudioBootstrapOptions): StudioBootstrapState {
|
|
19
|
+
const [state, setState] = React.useState<StudioBootstrapState>({
|
|
20
|
+
ready: false,
|
|
21
|
+
userId: null,
|
|
22
|
+
error: null,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
let cancelled = false;
|
|
27
|
+
|
|
28
|
+
(async () => {
|
|
29
|
+
try {
|
|
30
|
+
setClientApiKey(options.apiKey);
|
|
31
|
+
const requireAuth = isSupabaseClientInjected();
|
|
32
|
+
if (!requireAuth) {
|
|
33
|
+
const cfg = await studioConfigRepository.get();
|
|
34
|
+
setSupabaseConfig(cfg);
|
|
35
|
+
}
|
|
36
|
+
const { user } = requireAuth ? await ensureAuthenticatedSession() : await ensureAnonymousSession();
|
|
37
|
+
|
|
38
|
+
if (cancelled) return;
|
|
39
|
+
setState({ ready: true, userId: user.id, error: null });
|
|
40
|
+
} catch (e) {
|
|
41
|
+
if (cancelled) return;
|
|
42
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
43
|
+
setState({ ready: false, userId: null, error: err });
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
};
|
|
50
|
+
}, [options.apiKey]);
|
|
51
|
+
|
|
52
|
+
return state;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { App } from '../../data/apps/types';
|
|
4
|
+
import { appsRepository } from '../../data/apps/repository';
|
|
5
|
+
|
|
6
|
+
export type UseAppResult = {
|
|
7
|
+
app: App | null;
|
|
8
|
+
loading: boolean;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
refetch: () => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type UseAppOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* When false, this hook won't fetch or subscribe.
|
|
16
|
+
* Useful to avoid duplicate Supabase channel subscriptions for the same app id.
|
|
17
|
+
*/
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function useApp(appId: string, options?: UseAppOptions): UseAppResult {
|
|
22
|
+
const enabled = options?.enabled ?? true;
|
|
23
|
+
const [app, setApp] = React.useState<App | null>(null);
|
|
24
|
+
const [loading, setLoading] = React.useState(false);
|
|
25
|
+
const [error, setError] = React.useState<Error | null>(null);
|
|
26
|
+
|
|
27
|
+
const mergeApp = React.useCallback((prev: App | null, next: App): App => {
|
|
28
|
+
// Realtime (Supabase) rows don't include "viewer-specific" fields like `isLiked`,
|
|
29
|
+
// and may omit derived fields like `insights`. Preserve those from the last REST fetch.
|
|
30
|
+
const merged: App = {
|
|
31
|
+
...(prev ?? ({} as App)),
|
|
32
|
+
...next,
|
|
33
|
+
isLiked: next.isLiked ?? prev?.isLiked,
|
|
34
|
+
insights: next.insights ?? prev?.insights,
|
|
35
|
+
};
|
|
36
|
+
return merged;
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const fetchOnce = React.useCallback(async () => {
|
|
40
|
+
if (!enabled) return;
|
|
41
|
+
if (!appId) return;
|
|
42
|
+
setLoading(true);
|
|
43
|
+
setError(null);
|
|
44
|
+
try {
|
|
45
|
+
const next = await appsRepository.getById(appId);
|
|
46
|
+
setApp((prev) => mergeApp(prev, next));
|
|
47
|
+
} catch (e) {
|
|
48
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
49
|
+
setApp(null);
|
|
50
|
+
} finally {
|
|
51
|
+
setLoading(false);
|
|
52
|
+
}
|
|
53
|
+
}, [appId, enabled]);
|
|
54
|
+
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
if (!enabled) return;
|
|
57
|
+
void fetchOnce();
|
|
58
|
+
}, [enabled, fetchOnce]);
|
|
59
|
+
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
if (!enabled) return;
|
|
62
|
+
if (!appId) return;
|
|
63
|
+
const unsubscribe = appsRepository.subscribeApp(appId, {
|
|
64
|
+
onInsert: (a) => {
|
|
65
|
+
console.log('[useApp] onInsert', a);
|
|
66
|
+
setApp((prev) => mergeApp(prev, a));
|
|
67
|
+
},
|
|
68
|
+
onUpdate: (a) => {
|
|
69
|
+
console.log('[useApp] onUpdate', a);
|
|
70
|
+
setApp((prev) => mergeApp(prev, a));
|
|
71
|
+
},
|
|
72
|
+
onDelete: () => {
|
|
73
|
+
console.log('[useApp] onDelete');
|
|
74
|
+
setApp(null);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
return unsubscribe;
|
|
78
|
+
}, [appId, enabled, mergeApp]);
|
|
79
|
+
|
|
80
|
+
return { app, loading, error, refetch: fetchOnce };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as Haptics from 'expo-haptics';
|
|
3
|
+
|
|
4
|
+
import type { App } from '../../data/apps/types';
|
|
5
|
+
import { appLikesRepository } from '../../data/likes/repository';
|
|
6
|
+
|
|
7
|
+
export type UseAppStatsParams = {
|
|
8
|
+
appId: string;
|
|
9
|
+
initialLikes?: number;
|
|
10
|
+
initialComments?: number;
|
|
11
|
+
initialForks?: number;
|
|
12
|
+
initialIsLiked?: boolean;
|
|
13
|
+
onOpenComments?: () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type AppStatsResult = {
|
|
17
|
+
likeCount: number;
|
|
18
|
+
commentCount: number;
|
|
19
|
+
forkCount: number;
|
|
20
|
+
isLiked: boolean;
|
|
21
|
+
setCommentCount: (count: number) => void;
|
|
22
|
+
handleLike: () => Promise<void>;
|
|
23
|
+
handleOpenComments: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function useAppStats({
|
|
27
|
+
appId,
|
|
28
|
+
initialLikes = 0,
|
|
29
|
+
initialComments = 0,
|
|
30
|
+
initialForks = 0,
|
|
31
|
+
initialIsLiked = false,
|
|
32
|
+
onOpenComments,
|
|
33
|
+
}: UseAppStatsParams): AppStatsResult {
|
|
34
|
+
const [likeCount, setLikeCount] = React.useState(initialLikes);
|
|
35
|
+
const [commentCount, setCommentCount] = React.useState(initialComments);
|
|
36
|
+
const [forkCount, setForkCount] = React.useState(initialForks);
|
|
37
|
+
const [isLiked, setIsLiked] = React.useState(initialIsLiked);
|
|
38
|
+
|
|
39
|
+
const didMutateRef = React.useRef(false);
|
|
40
|
+
const lastAppIdRef = React.useRef<string>('');
|
|
41
|
+
React.useEffect(() => {
|
|
42
|
+
if (lastAppIdRef.current === appId) return;
|
|
43
|
+
lastAppIdRef.current = appId;
|
|
44
|
+
didMutateRef.current = false;
|
|
45
|
+
}, [appId]);
|
|
46
|
+
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
if (didMutateRef.current) return;
|
|
49
|
+
setLikeCount(initialLikes);
|
|
50
|
+
}, [appId, initialLikes]);
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
if (didMutateRef.current) return;
|
|
53
|
+
setCommentCount(initialComments);
|
|
54
|
+
}, [appId, initialComments]);
|
|
55
|
+
React.useEffect(() => {
|
|
56
|
+
if (didMutateRef.current) return;
|
|
57
|
+
setForkCount(initialForks);
|
|
58
|
+
}, [appId, initialForks]);
|
|
59
|
+
React.useEffect(() => {
|
|
60
|
+
if (didMutateRef.current) return;
|
|
61
|
+
setIsLiked(initialIsLiked);
|
|
62
|
+
}, [appId, initialIsLiked]);
|
|
63
|
+
|
|
64
|
+
const handleLike = React.useCallback(async () => {
|
|
65
|
+
if (!appId) return;
|
|
66
|
+
didMutateRef.current = true;
|
|
67
|
+
try {
|
|
68
|
+
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const newIsLiked = !isLiked;
|
|
73
|
+
setIsLiked(newIsLiked);
|
|
74
|
+
setLikeCount((prev) => Math.max(0, prev + (newIsLiked ? 1 : -1)));
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
if (newIsLiked) {
|
|
78
|
+
const res = await appLikesRepository.create(appId, {});
|
|
79
|
+
if (typeof res.stats?.total === 'number') setLikeCount(Math.max(0, res.stats.total));
|
|
80
|
+
} else {
|
|
81
|
+
const res = await appLikesRepository.removeMine(appId);
|
|
82
|
+
if (typeof res.stats?.total === 'number') setLikeCount(Math.max(0, res.stats.total));
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
setIsLiked(!newIsLiked);
|
|
86
|
+
setLikeCount((prev) => Math.max(0, prev + (newIsLiked ? -1 : 1)));
|
|
87
|
+
}
|
|
88
|
+
}, [appId, isLiked, likeCount]);
|
|
89
|
+
|
|
90
|
+
const handleOpenComments = React.useCallback(() => {
|
|
91
|
+
if (!appId) return;
|
|
92
|
+
try {
|
|
93
|
+
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
onOpenComments?.();
|
|
97
|
+
}, [appId, onOpenComments]);
|
|
98
|
+
|
|
99
|
+
return { likeCount, commentCount, forkCount, isLiked, setCommentCount, handleLike, handleOpenComments };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getAppStatsFromApp(app: App | null): Omit<UseAppStatsParams, 'appId'> {
|
|
103
|
+
return {
|
|
104
|
+
initialLikes: app?.insights?.totalLikes ?? 0,
|
|
105
|
+
initialComments: app?.insights?.totalComments ?? 0,
|
|
106
|
+
initialForks: app?.insights?.totalForks ?? 0,
|
|
107
|
+
initialIsLiked: Boolean(app?.isLiked),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|