@comergehq/studio 0.1.30 → 0.1.32
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.js +1525 -793
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1448 -715
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/assets/lottie/remix-x-loop.lottie.json +470 -0
- package/src/components/icons/RemixUpIcon.tsx +20 -0
- package/src/components/icons/RemixXLoopLottie.tsx +22 -0
- package/src/data/agent/remote.ts +8 -0
- package/src/data/agent/repository.ts +8 -0
- package/src/data/agent/types.ts +16 -0
- package/src/data/attachment/remote.ts +15 -0
- package/src/data/attachment/repository.ts +21 -0
- package/src/data/attachment/types.ts +20 -0
- package/src/studio/ComergeStudio.tsx +1 -0
- package/src/studio/hooks/useAttachmentUpload.ts +38 -1
- package/src/studio/hooks/useOptimisticChatMessages.ts +5 -5
- package/src/studio/hooks/useStudioActions.ts +50 -7
- package/src/studio/ui/ChatPanel.tsx +6 -6
- package/src/studio/ui/PreviewPanel.tsx +9 -5
- package/src/studio/ui/StudioOverlay.tsx +5 -3
- package/src/studio/ui/preview-panel/PreviewPanelHeader.tsx +37 -212
- package/src/studio/ui/preview-panel/PreviewRelatedAppsSection.tsx +285 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ActivityIndicator, Pressable, ScrollView, View } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { RelatedApp, RelatedApps } from '../../../data/apps/types';
|
|
5
|
+
import { Modal } from '../../../components/primitives/Modal';
|
|
6
|
+
import { Text } from '../../../components/primitives/Text';
|
|
7
|
+
import { PreviewStatusBadge } from '../../../components/preview/PreviewStatusBadge';
|
|
8
|
+
import { withAlpha } from '../../../components/utils/color';
|
|
9
|
+
import { useTheme } from '../../../theme';
|
|
10
|
+
import { SectionTitle } from './SectionTitle';
|
|
11
|
+
|
|
12
|
+
type RelatedAppListItem = {
|
|
13
|
+
app: RelatedApp;
|
|
14
|
+
section: 'original' | 'remix';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type PreviewRelatedAppsSectionProps = {
|
|
18
|
+
relatedApps?: RelatedApps | null;
|
|
19
|
+
relatedAppsLoading?: boolean;
|
|
20
|
+
switchingRelatedAppId?: string | null;
|
|
21
|
+
onOpenRelatedApps?: () => void;
|
|
22
|
+
onSwitchRelatedApp?: (targetAppId: string) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const INLINE_VISIBLE_COUNT = 4;
|
|
26
|
+
|
|
27
|
+
function formatRelativeUpdatedAt(updatedAt: string): string {
|
|
28
|
+
const parsed = new Date(updatedAt);
|
|
29
|
+
const ms = parsed.getTime();
|
|
30
|
+
if (!Number.isFinite(ms)) return 'Updated recently';
|
|
31
|
+
|
|
32
|
+
const diffMs = Date.now() - ms;
|
|
33
|
+
if (diffMs < 60_000) return 'Updated just now';
|
|
34
|
+
|
|
35
|
+
const minutes = Math.floor(diffMs / 60_000);
|
|
36
|
+
if (minutes < 60) return `Updated ${minutes}m ago`;
|
|
37
|
+
|
|
38
|
+
const hours = Math.floor(minutes / 60);
|
|
39
|
+
if (hours < 24) return `Updated ${hours}h ago`;
|
|
40
|
+
|
|
41
|
+
const days = Math.floor(hours / 24);
|
|
42
|
+
if (days === 1) return 'Updated yesterday';
|
|
43
|
+
if (days < 7) return `Updated ${days}d ago`;
|
|
44
|
+
|
|
45
|
+
return `Updated ${parsed.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function PreviewRelatedAppsSection({
|
|
49
|
+
relatedApps,
|
|
50
|
+
relatedAppsLoading,
|
|
51
|
+
switchingRelatedAppId,
|
|
52
|
+
onOpenRelatedApps,
|
|
53
|
+
onSwitchRelatedApp,
|
|
54
|
+
}: PreviewRelatedAppsSectionProps) {
|
|
55
|
+
const theme = useTheme();
|
|
56
|
+
const [relatedAppsOpen, setRelatedAppsOpen] = React.useState(false);
|
|
57
|
+
|
|
58
|
+
const relatedAppItems = React.useMemo((): RelatedAppListItem[] => {
|
|
59
|
+
if (!relatedApps) return [];
|
|
60
|
+
|
|
61
|
+
const items: RelatedAppListItem[] = [];
|
|
62
|
+
if (relatedApps.original) {
|
|
63
|
+
items.push({ app: relatedApps.original, section: 'original' });
|
|
64
|
+
}
|
|
65
|
+
for (const remix of relatedApps.remixes) {
|
|
66
|
+
items.push({ app: remix, section: 'remix' });
|
|
67
|
+
}
|
|
68
|
+
return items;
|
|
69
|
+
}, [relatedApps]);
|
|
70
|
+
|
|
71
|
+
const dedupedRelatedApps = React.useMemo(() => {
|
|
72
|
+
const seen = new Set<string>();
|
|
73
|
+
const items: RelatedAppListItem[] = [];
|
|
74
|
+
for (const item of relatedAppItems) {
|
|
75
|
+
if (seen.has(item.app.id)) continue;
|
|
76
|
+
seen.add(item.app.id);
|
|
77
|
+
items.push(item);
|
|
78
|
+
}
|
|
79
|
+
return items;
|
|
80
|
+
}, [relatedAppItems]);
|
|
81
|
+
|
|
82
|
+
const uniqueRelatedCount = dedupedRelatedApps.length;
|
|
83
|
+
const shouldShowRelatedApps = uniqueRelatedCount >= 2;
|
|
84
|
+
|
|
85
|
+
const currentAppId = relatedApps?.current.id;
|
|
86
|
+
const originalAppId = relatedApps?.original?.id ?? null;
|
|
87
|
+
|
|
88
|
+
const sectionedRelatedApps = React.useMemo(() => {
|
|
89
|
+
const original: RelatedAppListItem[] = [];
|
|
90
|
+
const remixes: RelatedAppListItem[] = [];
|
|
91
|
+
for (const item of dedupedRelatedApps) {
|
|
92
|
+
if (item.section === 'original') {
|
|
93
|
+
original.push(item);
|
|
94
|
+
} else {
|
|
95
|
+
remixes.push(item);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { original, remixes };
|
|
99
|
+
}, [dedupedRelatedApps]);
|
|
100
|
+
|
|
101
|
+
const inlineItems = React.useMemo(() => dedupedRelatedApps.slice(0, INLINE_VISIBLE_COUNT), [dedupedRelatedApps]);
|
|
102
|
+
const overflowCount = Math.max(0, uniqueRelatedCount - inlineItems.length);
|
|
103
|
+
const canOpenModal = overflowCount > 0;
|
|
104
|
+
|
|
105
|
+
const closeRelatedApps = React.useCallback(() => {
|
|
106
|
+
setRelatedAppsOpen(false);
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
const openRelatedApps = React.useCallback(() => {
|
|
110
|
+
if (!canOpenModal) return;
|
|
111
|
+
setRelatedAppsOpen(true);
|
|
112
|
+
onOpenRelatedApps?.();
|
|
113
|
+
}, [canOpenModal, onOpenRelatedApps]);
|
|
114
|
+
|
|
115
|
+
const handleSelectRelatedApp = React.useCallback(
|
|
116
|
+
(targetAppId: string) => {
|
|
117
|
+
if (!relatedApps) return;
|
|
118
|
+
if (targetAppId === relatedApps.current.id) return;
|
|
119
|
+
onSwitchRelatedApp?.(targetAppId);
|
|
120
|
+
setRelatedAppsOpen(false);
|
|
121
|
+
},
|
|
122
|
+
[onSwitchRelatedApp, relatedApps]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const renderBadges = React.useCallback(
|
|
126
|
+
(item: RelatedAppListItem, isCurrent: boolean) => {
|
|
127
|
+
const badges: React.ReactNode[] = [];
|
|
128
|
+
|
|
129
|
+
if (item.app.id === originalAppId) {
|
|
130
|
+
badges.push(
|
|
131
|
+
<View
|
|
132
|
+
key="original"
|
|
133
|
+
style={{ borderRadius: 999, paddingHorizontal: 8, paddingVertical: 2, backgroundColor: withAlpha(theme.colors.neutral, 0.4) }}
|
|
134
|
+
>
|
|
135
|
+
<Text style={{ color: theme.colors.textMuted, fontSize: 11 }}>Original</Text>
|
|
136
|
+
</View>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isCurrent) {
|
|
141
|
+
badges.push(
|
|
142
|
+
<View
|
|
143
|
+
key="current"
|
|
144
|
+
style={{ borderRadius: 999, paddingHorizontal: 8, paddingVertical: 2, backgroundColor: theme.colors.primary }}
|
|
145
|
+
>
|
|
146
|
+
<Text style={{ color: theme.colors.onPrimary, fontSize: 11 }}>Current</Text>
|
|
147
|
+
</View>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return badges;
|
|
152
|
+
},
|
|
153
|
+
[originalAppId, theme.colors.neutral, theme.colors.onPrimary, theme.colors.primary, theme.colors.textMuted]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const renderRelatedCard = React.useCallback(
|
|
157
|
+
(item: RelatedAppListItem, options?: { fullWidth?: boolean }) => {
|
|
158
|
+
const isCurrent = item.app.id === currentAppId;
|
|
159
|
+
const isReady = item.app.status === 'ready';
|
|
160
|
+
const isSwitching = switchingRelatedAppId === item.app.id;
|
|
161
|
+
const disabled = isCurrent || !isReady || Boolean(switchingRelatedAppId);
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<Pressable
|
|
165
|
+
key={item.app.id}
|
|
166
|
+
accessibilityRole="button"
|
|
167
|
+
accessibilityLabel={`Switch to ${item.app.name}, ${formatRelativeUpdatedAt(item.app.updatedAt).toLowerCase()}`}
|
|
168
|
+
disabled={disabled}
|
|
169
|
+
onPress={() => handleSelectRelatedApp(item.app.id)}
|
|
170
|
+
style={{
|
|
171
|
+
borderRadius: theme.radii.md,
|
|
172
|
+
borderWidth: 1,
|
|
173
|
+
borderColor: withAlpha(theme.colors.border, isCurrent ? 1 : 0.8),
|
|
174
|
+
backgroundColor: isCurrent ? withAlpha(theme.colors.primary, 0.09) : withAlpha(theme.colors.surfaceRaised, 0.5),
|
|
175
|
+
paddingHorizontal: theme.spacing.sm,
|
|
176
|
+
paddingVertical: 8,
|
|
177
|
+
opacity: disabled ? 0.7 : 1,
|
|
178
|
+
width: options?.fullWidth ? undefined : 188,
|
|
179
|
+
minWidth: options?.fullWidth ? undefined : 188,
|
|
180
|
+
marginBottom: options?.fullWidth ? theme.spacing.sm : 0,
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
<View style={{ flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', gap: 8 }}>
|
|
184
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
185
|
+
<Text numberOfLines={1} style={{ color: theme.colors.text, fontWeight: theme.typography.fontWeight.semibold }}>
|
|
186
|
+
{item.app.name}
|
|
187
|
+
</Text>
|
|
188
|
+
<Text style={{ marginTop: 2, color: theme.colors.textMuted, fontSize: 12 }}>
|
|
189
|
+
{formatRelativeUpdatedAt(item.app.updatedAt)}
|
|
190
|
+
</Text>
|
|
191
|
+
<View
|
|
192
|
+
style={{
|
|
193
|
+
marginTop: 4,
|
|
194
|
+
minHeight: 20,
|
|
195
|
+
flexDirection: 'row',
|
|
196
|
+
alignItems: 'center',
|
|
197
|
+
flexWrap: 'wrap',
|
|
198
|
+
gap: 6,
|
|
199
|
+
}}
|
|
200
|
+
>
|
|
201
|
+
{renderBadges(item, isCurrent)}
|
|
202
|
+
</View>
|
|
203
|
+
</View>
|
|
204
|
+
|
|
205
|
+
<View style={{ alignItems: 'flex-end', gap: 6 }}>
|
|
206
|
+
<View style={{ minHeight: 20, justifyContent: 'center' }}>
|
|
207
|
+
{!isReady ? <PreviewStatusBadge status={item.app.status} /> : null}
|
|
208
|
+
</View>
|
|
209
|
+
{isSwitching ? <ActivityIndicator size="small" color={theme.colors.primary} /> : null}
|
|
210
|
+
</View>
|
|
211
|
+
</View>
|
|
212
|
+
</Pressable>
|
|
213
|
+
);
|
|
214
|
+
},
|
|
215
|
+
[currentAppId, handleSelectRelatedApp, renderBadges, switchingRelatedAppId, theme]
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (!relatedAppsLoading && !shouldShowRelatedApps) return null;
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
<SectionTitle>Related Apps</SectionTitle>
|
|
223
|
+
<View
|
|
224
|
+
style={{
|
|
225
|
+
flexDirection: 'row',
|
|
226
|
+
alignItems: 'center',
|
|
227
|
+
justifyContent: 'flex-end',
|
|
228
|
+
marginBottom: theme.spacing.xs,
|
|
229
|
+
paddingHorizontal: theme.spacing.md,
|
|
230
|
+
}}
|
|
231
|
+
>
|
|
232
|
+
{canOpenModal ? (
|
|
233
|
+
<Pressable accessibilityRole="button" accessibilityLabel="Open all related apps" onPress={openRelatedApps}>
|
|
234
|
+
<Text style={{ color: theme.colors.primary, fontSize: 12, fontWeight: theme.typography.fontWeight.semibold }}>
|
|
235
|
+
See all ({uniqueRelatedCount})
|
|
236
|
+
</Text>
|
|
237
|
+
</Pressable>
|
|
238
|
+
) : null}
|
|
239
|
+
</View>
|
|
240
|
+
|
|
241
|
+
{relatedAppsLoading ? (
|
|
242
|
+
<View style={{ height: 72, alignItems: 'center', justifyContent: 'center', marginBottom: theme.spacing.xs }}>
|
|
243
|
+
<ActivityIndicator color={theme.colors.primary} />
|
|
244
|
+
</View>
|
|
245
|
+
) : (
|
|
246
|
+
<ScrollView
|
|
247
|
+
horizontal
|
|
248
|
+
showsHorizontalScrollIndicator={false}
|
|
249
|
+
style={{ flexGrow: 0 }}
|
|
250
|
+
contentContainerStyle={{
|
|
251
|
+
paddingHorizontal: theme.spacing.md,
|
|
252
|
+
gap: theme.spacing.sm,
|
|
253
|
+
paddingBottom: theme.spacing.xs,
|
|
254
|
+
alignItems: 'flex-start',
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
{inlineItems.map((item) => renderRelatedCard(item))}
|
|
258
|
+
</ScrollView>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
<Modal visible={relatedAppsOpen} onRequestClose={closeRelatedApps}>
|
|
262
|
+
<View style={{ gap: theme.spacing.sm }}>
|
|
263
|
+
<Text style={{ color: theme.colors.text, fontSize: 18, fontWeight: theme.typography.fontWeight.semibold }}>
|
|
264
|
+
Related apps
|
|
265
|
+
</Text>
|
|
266
|
+
|
|
267
|
+
{sectionedRelatedApps.original.length > 0 ? (
|
|
268
|
+
<View>
|
|
269
|
+
<Text style={{ color: theme.colors.textMuted, marginBottom: theme.spacing.xs }}>Original</Text>
|
|
270
|
+
{sectionedRelatedApps.original.map((item) => renderRelatedCard(item, { fullWidth: true }))}
|
|
271
|
+
</View>
|
|
272
|
+
) : null}
|
|
273
|
+
|
|
274
|
+
{sectionedRelatedApps.remixes.length > 0 ? (
|
|
275
|
+
<View>
|
|
276
|
+
<Text style={{ color: theme.colors.textMuted, marginBottom: theme.spacing.xs }}>Remixes</Text>
|
|
277
|
+
{sectionedRelatedApps.remixes.map((item) => renderRelatedCard(item, { fullWidth: true }))}
|
|
278
|
+
</View>
|
|
279
|
+
) : null}
|
|
280
|
+
</View>
|
|
281
|
+
</Modal>
|
|
282
|
+
</>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|