@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,96 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { App } from '../../data/apps/types';
|
|
4
|
+
import { appsRepository } from '../../data/apps/repository';
|
|
5
|
+
import { agentRepository } from '../../data/agent/repository';
|
|
6
|
+
import type { AttachmentMeta } from '../../data/attachment/types';
|
|
7
|
+
|
|
8
|
+
export type UseStudioActionsParams = {
|
|
9
|
+
userId: string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Current app object for the active appId.
|
|
12
|
+
*/
|
|
13
|
+
app: App | null;
|
|
14
|
+
/**
|
|
15
|
+
* Called when we fork and should switch to the new app.
|
|
16
|
+
*/
|
|
17
|
+
onForkedApp?: (appId: string, opts?: { keepRenderingAppId?: string }) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Upload function used to convert attachments.
|
|
20
|
+
*/
|
|
21
|
+
uploadAttachments?: (params: { threadId: string; appId: string; dataUrls: string[] }) => Promise<AttachmentMeta[]>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type UseStudioActionsResult = {
|
|
25
|
+
isOwner: boolean;
|
|
26
|
+
shouldForkOnEdit: boolean;
|
|
27
|
+
forking: boolean;
|
|
28
|
+
sending: boolean;
|
|
29
|
+
error: Error | null;
|
|
30
|
+
sendEdit: (params: { prompt: string; attachments?: string[] }) => Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function useStudioActions({
|
|
34
|
+
userId,
|
|
35
|
+
app,
|
|
36
|
+
onForkedApp,
|
|
37
|
+
uploadAttachments,
|
|
38
|
+
}: UseStudioActionsParams): UseStudioActionsResult {
|
|
39
|
+
const [forking, setForking] = React.useState(false);
|
|
40
|
+
const [sending, setSending] = React.useState(false);
|
|
41
|
+
const [error, setError] = React.useState<Error | null>(null);
|
|
42
|
+
|
|
43
|
+
const isOwner = Boolean(userId && app?.createdBy && userId === app.createdBy);
|
|
44
|
+
const shouldForkOnEdit = Boolean(userId && app && app.createdBy !== userId);
|
|
45
|
+
|
|
46
|
+
const sendEdit = React.useCallback(
|
|
47
|
+
async ({ prompt, attachments }: { prompt: string; attachments?: string[] }) => {
|
|
48
|
+
if (!userId || !app) return;
|
|
49
|
+
if (!prompt.trim()) return;
|
|
50
|
+
if (sending) return;
|
|
51
|
+
|
|
52
|
+
setSending(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
let targetApp = app;
|
|
56
|
+
|
|
57
|
+
if (shouldForkOnEdit) {
|
|
58
|
+
setForking(true);
|
|
59
|
+
const sourceAppId = app.id;
|
|
60
|
+
const forked = await appsRepository.fork(app.id, {});
|
|
61
|
+
targetApp = forked;
|
|
62
|
+
// For fork+edit, keep rendering the original app until the edit completes on the fork.
|
|
63
|
+
onForkedApp?.(forked.id, { keepRenderingAppId: sourceAppId });
|
|
64
|
+
}
|
|
65
|
+
setForking(false);
|
|
66
|
+
|
|
67
|
+
const threadId = targetApp.threadId;
|
|
68
|
+
if (!threadId) throw new Error('No thread available for this app.');
|
|
69
|
+
|
|
70
|
+
let attachmentMetas: AttachmentMeta[] | undefined;
|
|
71
|
+
if (attachments && attachments.length > 0 && uploadAttachments) {
|
|
72
|
+
attachmentMetas = await uploadAttachments({ threadId, appId: targetApp.id, dataUrls: attachments });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await agentRepository.editApp({
|
|
76
|
+
prompt,
|
|
77
|
+
thread_id: threadId,
|
|
78
|
+
app_id: targetApp.id,
|
|
79
|
+
attachments: attachmentMetas && attachmentMetas.length > 0 ? attachmentMetas : undefined,
|
|
80
|
+
});
|
|
81
|
+
} catch (e) {
|
|
82
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
83
|
+
setError(err);
|
|
84
|
+
throw err;
|
|
85
|
+
} finally {
|
|
86
|
+
setForking(false);
|
|
87
|
+
setSending(false);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
[app, onForkedApp, sending, shouldForkOnEdit, uploadAttachments, userId]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return { isOwner, shouldForkOnEdit, forking, sending, error, sendEdit };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Message } from '../../data/messages/types';
|
|
4
|
+
import { messagesRepository } from '../../data/messages/repository';
|
|
5
|
+
import type { ChatMessage } from '../../components/models/types';
|
|
6
|
+
|
|
7
|
+
export type UseThreadMessagesResult = {
|
|
8
|
+
raw: Message[];
|
|
9
|
+
messages: ChatMessage[];
|
|
10
|
+
loading: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
refetch: () => Promise<void>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function extractMeta(payload: unknown): ChatMessage['meta'] {
|
|
16
|
+
const meta = (payload as any)?.meta;
|
|
17
|
+
if (!meta || typeof meta !== 'object') return null;
|
|
18
|
+
const obj = meta as Record<string, unknown>;
|
|
19
|
+
return {
|
|
20
|
+
kind: typeof obj.kind === 'string' ? obj.kind : undefined,
|
|
21
|
+
event: typeof obj.event === 'string' ? obj.event : undefined,
|
|
22
|
+
status: typeof obj.status === 'string' ? (obj.status as any) : undefined,
|
|
23
|
+
mergeRequestId: typeof obj.mergeRequestId === 'string' ? obj.mergeRequestId : undefined,
|
|
24
|
+
sourceAppId: typeof obj.sourceAppId === 'string' ? obj.sourceAppId : undefined,
|
|
25
|
+
targetAppId: typeof obj.targetAppId === 'string' ? obj.targetAppId : undefined,
|
|
26
|
+
appId: typeof obj.appId === 'string' ? obj.appId : undefined,
|
|
27
|
+
threadId: typeof obj.threadId === 'string' ? obj.threadId : undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mapMessageToChatMessage(m: Message): ChatMessage {
|
|
32
|
+
const kind = typeof (m.payload as any)?.type === 'string' ? String((m.payload as any).type) : null;
|
|
33
|
+
return {
|
|
34
|
+
id: m.id,
|
|
35
|
+
author: m.authorType === 'ai' ? 'assistant' : 'human',
|
|
36
|
+
content: typeof m.payload?.content === 'string' ? m.payload.content : '',
|
|
37
|
+
createdAt: m.createdAt,
|
|
38
|
+
kind,
|
|
39
|
+
meta: extractMeta(m.payload),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useThreadMessages(threadId: string): UseThreadMessagesResult {
|
|
44
|
+
const [raw, setRaw] = React.useState<Message[]>([]);
|
|
45
|
+
const [loading, setLoading] = React.useState(false);
|
|
46
|
+
const [error, setError] = React.useState<Error | null>(null);
|
|
47
|
+
|
|
48
|
+
const refetch = React.useCallback(async () => {
|
|
49
|
+
if (!threadId) {
|
|
50
|
+
setRaw([]);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setLoading(true);
|
|
54
|
+
setError(null);
|
|
55
|
+
try {
|
|
56
|
+
const list = await messagesRepository.list(threadId);
|
|
57
|
+
setRaw(list);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
60
|
+
setRaw([]);
|
|
61
|
+
} finally {
|
|
62
|
+
setLoading(false);
|
|
63
|
+
}
|
|
64
|
+
}, [threadId]);
|
|
65
|
+
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
void refetch();
|
|
68
|
+
}, [refetch]);
|
|
69
|
+
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
if (!threadId) return;
|
|
72
|
+
const unsubscribe = messagesRepository.subscribeThread(threadId, {
|
|
73
|
+
onInsert: (m) => setRaw((prev) => [...prev, m]),
|
|
74
|
+
onUpdate: (m) => setRaw((prev) => prev.map((x) => (x.id === m.id ? m : x))),
|
|
75
|
+
onDelete: (m) => setRaw((prev) => prev.filter((x) => x.id !== m.id)),
|
|
76
|
+
});
|
|
77
|
+
return unsubscribe;
|
|
78
|
+
}, [threadId]);
|
|
79
|
+
|
|
80
|
+
const messages = React.useMemo(() => raw.map(mapMessageToChatMessage), [raw]);
|
|
81
|
+
|
|
82
|
+
return { raw, messages, loading, error, refetch };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Message } from '../../data/messages/types';
|
|
2
|
+
|
|
3
|
+
type MessagePayload = { type?: string; content?: unknown; text?: unknown; prompt?: unknown; message?: unknown };
|
|
4
|
+
|
|
5
|
+
export function getLastOutcomeIndex(messages: Message[]): number {
|
|
6
|
+
let idx = -1;
|
|
7
|
+
for (let i = 0; i < messages.length; i += 1) {
|
|
8
|
+
const payload = messages[i].payload as MessagePayload | undefined;
|
|
9
|
+
if (payload?.type === 'outcome') idx = i;
|
|
10
|
+
}
|
|
11
|
+
return idx;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasNoOutcomeAfterLastHuman(messages: Message[]): boolean {
|
|
15
|
+
if (!messages || messages.length === 0) return false;
|
|
16
|
+
|
|
17
|
+
let lastHumanIndex = -1;
|
|
18
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
19
|
+
if (messages[i].authorType === 'human') {
|
|
20
|
+
lastHumanIndex = i;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (lastHumanIndex === -1) return false;
|
|
25
|
+
|
|
26
|
+
for (let i = lastHumanIndex + 1; i < messages.length; i += 1) {
|
|
27
|
+
const m = messages[i];
|
|
28
|
+
const payload = m.payload as MessagePayload | undefined;
|
|
29
|
+
if (m.authorType === 'ai' && payload?.type === 'outcome') return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { ChatMessageListRef } from '../../components/chat/ChatMessageList';
|
|
5
|
+
import { ChatPage } from '../../components/chat/ChatPage';
|
|
6
|
+
import { ScrollToBottomButton } from '../../components/chat/ScrollToBottomButton';
|
|
7
|
+
import { ChatHeader } from '../../components/chat/ChatHeader';
|
|
8
|
+
import { ForkNoticeBanner } from '../../components/chat/ForkNoticeBanner';
|
|
9
|
+
import { StudioSheetHeaderIconButton } from '../../components/studio-sheet/StudioSheetHeaderIconButton';
|
|
10
|
+
import { IconArrowDown, IconBack, IconClose, IconDraw, IconHome } from '../../components/icons/StudioIcons';
|
|
11
|
+
import { Text } from '../../components/primitives/Text';
|
|
12
|
+
import type { ChatMessage } from '../../components/models/types';
|
|
13
|
+
|
|
14
|
+
export type ChatPanelProps = {
|
|
15
|
+
title?: string;
|
|
16
|
+
autoFocusComposer?: boolean;
|
|
17
|
+
messages: ChatMessage[];
|
|
18
|
+
showTypingIndicator?: boolean;
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
sendDisabled?: boolean;
|
|
21
|
+
forking?: boolean;
|
|
22
|
+
sending?: boolean;
|
|
23
|
+
shouldForkOnEdit?: boolean;
|
|
24
|
+
attachments?: string[];
|
|
25
|
+
onRemoveAttachment?: (index: number) => void;
|
|
26
|
+
onClearAttachments?: () => void;
|
|
27
|
+
onBack: () => void;
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
onNavigateHome?: () => void;
|
|
30
|
+
onStartDraw?: () => void;
|
|
31
|
+
onSend: (text: string, attachments?: string[]) => void | Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function ChatPanel({
|
|
35
|
+
title = 'Chat',
|
|
36
|
+
autoFocusComposer = false,
|
|
37
|
+
messages,
|
|
38
|
+
showTypingIndicator,
|
|
39
|
+
loading,
|
|
40
|
+
sendDisabled,
|
|
41
|
+
forking = false,
|
|
42
|
+
sending,
|
|
43
|
+
shouldForkOnEdit,
|
|
44
|
+
attachments = [],
|
|
45
|
+
onRemoveAttachment,
|
|
46
|
+
onClearAttachments,
|
|
47
|
+
onBack,
|
|
48
|
+
onClose,
|
|
49
|
+
onNavigateHome,
|
|
50
|
+
onStartDraw,
|
|
51
|
+
onSend,
|
|
52
|
+
}: ChatPanelProps) {
|
|
53
|
+
const listRef = React.useRef<ChatMessageListRef | null>(null);
|
|
54
|
+
const [nearBottom, setNearBottom] = React.useState(true);
|
|
55
|
+
|
|
56
|
+
const handleSend = React.useCallback(
|
|
57
|
+
async (text: string, composerAttachments?: string[]) => {
|
|
58
|
+
const all = composerAttachments ?? attachments;
|
|
59
|
+
await onSend(text, all.length > 0 ? all : undefined);
|
|
60
|
+
onClearAttachments?.();
|
|
61
|
+
requestAnimationFrame(() => listRef.current?.scrollToBottom({ animated: true }));
|
|
62
|
+
},
|
|
63
|
+
[attachments, onClearAttachments, onSend]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const handleScrollToBottom = React.useCallback(() => {
|
|
67
|
+
listRef.current?.scrollToBottom({ animated: true });
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const header = (
|
|
71
|
+
<ChatHeader
|
|
72
|
+
left={
|
|
73
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
74
|
+
<StudioSheetHeaderIconButton onPress={onBack} accessibilityLabel="Back" style={{ marginRight: 8 }}>
|
|
75
|
+
<IconBack size={20} colorToken="floatingContent" />
|
|
76
|
+
</StudioSheetHeaderIconButton>
|
|
77
|
+
{onNavigateHome ? (
|
|
78
|
+
<StudioSheetHeaderIconButton onPress={onNavigateHome} accessibilityLabel="Home">
|
|
79
|
+
<IconHome size={20} colorToken="floatingContent" />
|
|
80
|
+
</StudioSheetHeaderIconButton>
|
|
81
|
+
) : null}
|
|
82
|
+
</View>
|
|
83
|
+
}
|
|
84
|
+
right={
|
|
85
|
+
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
86
|
+
{onStartDraw ? (
|
|
87
|
+
<StudioSheetHeaderIconButton onPress={onStartDraw} accessibilityLabel="Draw" intent="danger" style={{ marginRight: 8 }}>
|
|
88
|
+
<IconDraw size={20} colorToken="onDanger" />
|
|
89
|
+
</StudioSheetHeaderIconButton>
|
|
90
|
+
) : null}
|
|
91
|
+
<StudioSheetHeaderIconButton onPress={onClose} accessibilityLabel="Close">
|
|
92
|
+
<IconClose size={20} colorToken="floatingContent" />
|
|
93
|
+
</StudioSheetHeaderIconButton>
|
|
94
|
+
</View>
|
|
95
|
+
}
|
|
96
|
+
center={null}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const topBanner =
|
|
101
|
+
shouldForkOnEdit ? (
|
|
102
|
+
<ForkNoticeBanner
|
|
103
|
+
isOwner={!shouldForkOnEdit}
|
|
104
|
+
style={{ marginBottom: 12 }}
|
|
105
|
+
/>
|
|
106
|
+
) : null;
|
|
107
|
+
|
|
108
|
+
const showMessagesLoading = (Boolean(loading) && messages.length === 0) || forking;
|
|
109
|
+
if (showMessagesLoading) {
|
|
110
|
+
return (
|
|
111
|
+
<View style={{ flex: 1 }}>
|
|
112
|
+
<View>{header}</View>
|
|
113
|
+
{topBanner ? <View style={{ paddingHorizontal: 16, paddingTop: 8 }}>{topBanner}</View> : null}
|
|
114
|
+
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 24, paddingVertical: 12 }}>
|
|
115
|
+
<ActivityIndicator />
|
|
116
|
+
<View style={{ height: 12 }} />
|
|
117
|
+
<Text variant="bodyMuted">{forking ? 'Creating your copy…' : 'Loading messages…'}</Text>
|
|
118
|
+
</View>
|
|
119
|
+
</View>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<ChatPage
|
|
125
|
+
header={header}
|
|
126
|
+
messages={messages}
|
|
127
|
+
showTypingIndicator={showTypingIndicator}
|
|
128
|
+
topBanner={topBanner}
|
|
129
|
+
listRef={listRef}
|
|
130
|
+
onNearBottomChange={setNearBottom}
|
|
131
|
+
overlay={
|
|
132
|
+
<ScrollToBottomButton
|
|
133
|
+
visible={!nearBottom}
|
|
134
|
+
onPress={handleScrollToBottom}
|
|
135
|
+
style={{ bottom: 80 }}
|
|
136
|
+
>
|
|
137
|
+
<IconArrowDown size={20} colorToken="floatingContent" />
|
|
138
|
+
</ScrollToBottomButton>
|
|
139
|
+
}
|
|
140
|
+
composer={{
|
|
141
|
+
disabled: Boolean(loading) || Boolean(sendDisabled) || Boolean(forking),
|
|
142
|
+
sending: Boolean(sending),
|
|
143
|
+
autoFocus: autoFocusComposer,
|
|
144
|
+
onSend: handleSend,
|
|
145
|
+
attachments,
|
|
146
|
+
onRemoveAttachment: onRemoveAttachment,
|
|
147
|
+
onAddAttachment: onStartDraw,
|
|
148
|
+
useBottomSheetTextInput: true,
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { MergeRequest } from '../../data/merge-requests/types';
|
|
4
|
+
import { ConfirmMergeRequestDialog } from '../../components/dialogs/ConfirmMergeRequestDialog';
|
|
5
|
+
import type { MergeRequestSummary } from '../../components/models/types';
|
|
6
|
+
|
|
7
|
+
export type ConfirmMergeFlowProps = {
|
|
8
|
+
visible: boolean;
|
|
9
|
+
onOpenChange: (open: boolean) => void;
|
|
10
|
+
mergeRequest: MergeRequest | null;
|
|
11
|
+
toSummary: (mr: MergeRequest) => MergeRequestSummary;
|
|
12
|
+
/**
|
|
13
|
+
* Disable the primary "Approve Merge" action (e.g. while submitting).
|
|
14
|
+
*/
|
|
15
|
+
approveDisabled?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the system is building/preparing a test bundle.
|
|
18
|
+
* Disables the "Test edits first" action and shows "Preparing…".
|
|
19
|
+
*/
|
|
20
|
+
isBuilding?: boolean;
|
|
21
|
+
onConfirm: (mr: MergeRequest) => void | Promise<void>;
|
|
22
|
+
onTestFirst: (mr: MergeRequest) => void | Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function ConfirmMergeFlow({
|
|
26
|
+
visible,
|
|
27
|
+
onOpenChange,
|
|
28
|
+
mergeRequest,
|
|
29
|
+
toSummary,
|
|
30
|
+
approveDisabled,
|
|
31
|
+
isBuilding,
|
|
32
|
+
onConfirm,
|
|
33
|
+
onTestFirst,
|
|
34
|
+
}: ConfirmMergeFlowProps) {
|
|
35
|
+
return (
|
|
36
|
+
<ConfirmMergeRequestDialog
|
|
37
|
+
visible={visible}
|
|
38
|
+
onOpenChange={onOpenChange}
|
|
39
|
+
mergeRequest={mergeRequest ? toSummary(mergeRequest) : null}
|
|
40
|
+
approveDisabled={approveDisabled}
|
|
41
|
+
isBuilding={isBuilding}
|
|
42
|
+
onConfirm={() => {
|
|
43
|
+
if (!mergeRequest) return;
|
|
44
|
+
return onConfirm(mergeRequest);
|
|
45
|
+
}}
|
|
46
|
+
onTestFirst={(mrSummary) => {
|
|
47
|
+
if (!mergeRequest) return;
|
|
48
|
+
void mrSummary;
|
|
49
|
+
return onTestFirst(mergeRequest);
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ActivityIndicator, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { App } from '../../data/apps/types';
|
|
5
|
+
import type { MergeRequest } from '../../data/merge-requests/types';
|
|
6
|
+
import { PreviewPage } from '../../components/preview/PreviewPage';
|
|
7
|
+
import { Text } from '../../components/primitives/Text';
|
|
8
|
+
import { PreviewPanelHeader } from './preview-panel/PreviewPanelHeader';
|
|
9
|
+
import { PreviewHeroSection } from './preview-panel/PreviewHeroSection';
|
|
10
|
+
import { PreviewMetaSection } from './preview-panel/PreviewMetaSection';
|
|
11
|
+
import { PreviewCustomizeSection } from './preview-panel/PreviewCustomizeSection';
|
|
12
|
+
import { PreviewCollaborateSection } from './preview-panel/PreviewCollaborateSection';
|
|
13
|
+
import { usePreviewPanelData } from './preview-panel/usePreviewPanelData';
|
|
14
|
+
|
|
15
|
+
export type PreviewPanelProps = {
|
|
16
|
+
app: App | null;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
isOwner: boolean;
|
|
19
|
+
shouldForkOnEdit: boolean;
|
|
20
|
+
incomingMergeRequests: MergeRequest[];
|
|
21
|
+
outgoingMergeRequests: MergeRequest[];
|
|
22
|
+
creatorStatsById: Record<string, import('../../data/users/types').UserStats>;
|
|
23
|
+
processingMrId?: string | null;
|
|
24
|
+
isBuildingMrTest?: boolean;
|
|
25
|
+
testingMrId?: string | null;
|
|
26
|
+
toMergeRequestSummary: (mr: MergeRequest) => import('../../components/models/types').MergeRequestSummary;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
onNavigateHome?: () => void;
|
|
29
|
+
onGoToChat: () => void;
|
|
30
|
+
onStartDraw?: () => void;
|
|
31
|
+
onSubmitMergeRequest?: () => void | Promise<void>;
|
|
32
|
+
onRequestApprove?: (mr: MergeRequest) => void;
|
|
33
|
+
onReject?: (mr: MergeRequest) => void | Promise<void>;
|
|
34
|
+
onTestMr?: (mr: MergeRequest) => void | Promise<void>;
|
|
35
|
+
onOpenComments?: () => void;
|
|
36
|
+
commentCountOverride?: number;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function PreviewPanel({
|
|
40
|
+
app,
|
|
41
|
+
loading,
|
|
42
|
+
isOwner,
|
|
43
|
+
shouldForkOnEdit,
|
|
44
|
+
incomingMergeRequests,
|
|
45
|
+
outgoingMergeRequests,
|
|
46
|
+
creatorStatsById,
|
|
47
|
+
processingMrId,
|
|
48
|
+
isBuildingMrTest,
|
|
49
|
+
testingMrId,
|
|
50
|
+
toMergeRequestSummary,
|
|
51
|
+
onClose,
|
|
52
|
+
onNavigateHome,
|
|
53
|
+
onGoToChat,
|
|
54
|
+
onStartDraw,
|
|
55
|
+
onSubmitMergeRequest,
|
|
56
|
+
onRequestApprove,
|
|
57
|
+
onReject,
|
|
58
|
+
onTestMr,
|
|
59
|
+
onOpenComments,
|
|
60
|
+
commentCountOverride,
|
|
61
|
+
}: PreviewPanelProps) {
|
|
62
|
+
const { imageUrl, imageLoaded, setImageLoaded, creator, insights, stats, showProcessing, canSubmitMergeRequest } = usePreviewPanelData({
|
|
63
|
+
app,
|
|
64
|
+
isOwner,
|
|
65
|
+
outgoingMergeRequests,
|
|
66
|
+
onOpenComments,
|
|
67
|
+
commentCountOverride,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const header = <PreviewPanelHeader isOwner={isOwner} onClose={onClose} onNavigateHome={onNavigateHome} onGoToChat={onGoToChat} />;
|
|
71
|
+
|
|
72
|
+
if (loading || !app) {
|
|
73
|
+
return (
|
|
74
|
+
<PreviewPage header={header}>
|
|
75
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 }}>
|
|
76
|
+
<ActivityIndicator />
|
|
77
|
+
<View style={{ height: 12 }} />
|
|
78
|
+
<Text variant="bodyMuted">Loading app…</Text>
|
|
79
|
+
</View>
|
|
80
|
+
</PreviewPage>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<PreviewPage header={header}>
|
|
86
|
+
<PreviewHeroSection
|
|
87
|
+
appStatus={app.status}
|
|
88
|
+
showProcessing={showProcessing}
|
|
89
|
+
imageUrl={imageUrl}
|
|
90
|
+
imageLoaded={imageLoaded}
|
|
91
|
+
onImageLoad={() => setImageLoaded(true)}
|
|
92
|
+
stats={{
|
|
93
|
+
likeCount: stats.likeCount,
|
|
94
|
+
commentCount: stats.commentCount,
|
|
95
|
+
forkCount: stats.forkCount,
|
|
96
|
+
isLiked: stats.isLiked,
|
|
97
|
+
handleLike: stats.handleLike,
|
|
98
|
+
handleOpenComments: stats.handleOpenComments,
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
|
|
102
|
+
<PreviewMetaSection app={app} isOwner={isOwner} creator={creator} downloadsCount={insights.downloads} />
|
|
103
|
+
|
|
104
|
+
<PreviewCustomizeSection
|
|
105
|
+
app={app}
|
|
106
|
+
isOwner={isOwner}
|
|
107
|
+
shouldForkOnEdit={shouldForkOnEdit}
|
|
108
|
+
showProcessing={showProcessing}
|
|
109
|
+
onGoToChat={onGoToChat}
|
|
110
|
+
onStartDraw={onStartDraw}
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
<PreviewCollaborateSection
|
|
114
|
+
canSubmitMergeRequest={canSubmitMergeRequest}
|
|
115
|
+
incomingMergeRequests={incomingMergeRequests}
|
|
116
|
+
outgoingMergeRequests={outgoingMergeRequests}
|
|
117
|
+
creatorStatsById={creatorStatsById}
|
|
118
|
+
processingMrId={processingMrId}
|
|
119
|
+
isBuildingMrTest={isBuildingMrTest}
|
|
120
|
+
testingMrId={testingMrId}
|
|
121
|
+
toMergeRequestSummary={toMergeRequestSummary}
|
|
122
|
+
onSubmitMergeRequest={onSubmitMergeRequest}
|
|
123
|
+
onRequestApprove={onRequestApprove}
|
|
124
|
+
onReject={onReject}
|
|
125
|
+
onTestMr={onTestMr}
|
|
126
|
+
/>
|
|
127
|
+
</PreviewPage>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { View, type ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { ComergeRuntimeRenderer } from '@comergehq/runtime';
|
|
5
|
+
|
|
6
|
+
import { Text } from '../../components/primitives/Text';
|
|
7
|
+
|
|
8
|
+
export type RuntimeRendererProps = {
|
|
9
|
+
appKey: string;
|
|
10
|
+
bundlePath: string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Used to force a runtime remount even when bundlePath stays constant
|
|
13
|
+
* (e.g. base bundle replaced in-place).
|
|
14
|
+
*/
|
|
15
|
+
renderToken?: number;
|
|
16
|
+
style?: ViewStyle;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function RuntimeRenderer({ appKey, bundlePath, renderToken, style }: RuntimeRendererProps) {
|
|
20
|
+
if (!bundlePath) {
|
|
21
|
+
return (
|
|
22
|
+
<View style={[{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 }, style]}>
|
|
23
|
+
<Text variant="bodyMuted">Preparing app…</Text>
|
|
24
|
+
</View>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<View style={[{ flex: 1 }, style]}>
|
|
30
|
+
<ComergeRuntimeRenderer
|
|
31
|
+
key={`${appKey}:${bundlePath}:${renderToken ?? 0}`}
|
|
32
|
+
appKey={appKey}
|
|
33
|
+
bundlePath={bundlePath}
|
|
34
|
+
style={{ flex: 1 }}
|
|
35
|
+
/>
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|