@assistkick/create 1.7.0 → 1.8.0

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.
Files changed (200) hide show
  1. package/dist/bin/create.js +0 -0
  2. package/package.json +9 -7
  3. package/templates/assistkick-product-system/.env.example +1 -0
  4. package/templates/assistkick-product-system/local.db +0 -0
  5. package/templates/assistkick-product-system/package.json +4 -2
  6. package/templates/assistkick-product-system/packages/backend/package.json +2 -0
  7. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +165 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +358 -0
  9. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +356 -0
  10. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +96 -1
  11. package/templates/assistkick-product-system/packages/backend/src/routes/graph.ts +1 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/kanban.ts +43 -4
  13. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +200 -84
  14. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +6 -3
  15. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +53 -17
  16. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +218 -0
  17. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +119 -0
  18. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +154 -0
  19. package/templates/assistkick-product-system/packages/backend/src/server.ts +81 -9
  20. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +489 -0
  21. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +416 -0
  22. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.test.ts +189 -0
  23. package/templates/assistkick-product-system/packages/backend/src/services/bundle_service.ts +182 -0
  24. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +28 -78
  25. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +16 -0
  26. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +73 -2
  27. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +4 -4
  28. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +87 -11
  29. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +210 -69
  30. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +210 -215
  31. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +162 -0
  32. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +148 -0
  33. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +11 -5
  34. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.test.ts +64 -0
  35. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +134 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.test.ts +256 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +258 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/workflow_group_service.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.test.ts +275 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/workflow_service.ts +222 -0
  41. package/templates/assistkick-product-system/packages/frontend/package-lock.json +3455 -0
  42. package/templates/assistkick-product-system/packages/frontend/package.json +6 -0
  43. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +8 -0
  44. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +456 -16
  45. package/templates/assistkick-product-system/packages/frontend/src/api/client_files.test.ts +172 -0
  46. package/templates/assistkick-product-system/packages/frontend/src/api/client_video.test.ts +238 -0
  47. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +307 -0
  48. package/templates/assistkick-product-system/packages/frontend/src/components/CoherenceView.tsx +82 -66
  49. package/templates/assistkick-product-system/packages/frontend/src/components/CompositionPlaceholder.tsx +97 -0
  50. package/templates/assistkick-product-system/packages/frontend/src/components/DesignSystemView.tsx +20 -0
  51. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +57 -0
  52. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +313 -0
  53. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeContextMenu.tsx +61 -0
  54. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +73 -0
  55. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +404 -0
  56. package/templates/assistkick-product-system/packages/frontend/src/components/GitRepoModal.tsx +187 -56
  57. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +71 -73
  58. package/templates/assistkick-product-system/packages/frontend/src/components/GraphSettings.tsx +8 -8
  59. package/templates/assistkick-product-system/packages/frontend/src/components/GraphView.tsx +1 -1
  60. package/templates/assistkick-product-system/packages/frontend/src/components/InviteUserDialog.tsx +15 -11
  61. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +202 -171
  62. package/templates/assistkick-product-system/packages/frontend/src/components/LoginPage.tsx +14 -14
  63. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +54 -33
  64. package/templates/assistkick-product-system/packages/frontend/src/components/QaIssueSheet.tsx +32 -49
  65. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +43 -48
  66. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +121 -52
  67. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +20 -14
  68. package/templates/assistkick-product-system/packages/frontend/src/components/UsersView.tsx +52 -52
  69. package/templates/assistkick-product-system/packages/frontend/src/components/VideoGallery.tsx +313 -0
  70. package/templates/assistkick-product-system/packages/frontend/src/components/VideographyView.tsx +250 -0
  71. package/templates/assistkick-product-system/packages/frontend/src/components/WorkflowsView.tsx +474 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/ds/AccentBorderList.tsx +53 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Button.tsx +87 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonGroup.tsx +29 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ButtonShowcase.tsx +221 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CardGlass.tsx +141 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ds/CompletionRing.tsx +30 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ds/ContentCard.tsx +34 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ds/IconButton.tsx +74 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCard.tsx +103 -87
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KanbanCardShowcase.tsx +9 -188
  82. package/templates/assistkick-product-system/packages/frontend/src/components/ds/Kbd.tsx +11 -0
  83. package/templates/assistkick-product-system/packages/frontend/src/components/ds/KindBadge.tsx +21 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/ds/NavBarSidekick.tsx +81 -37
  85. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SidePanelShowcase.tsx +370 -0
  86. package/templates/assistkick-product-system/packages/frontend/src/components/ds/SideSheet.tsx +64 -0
  87. package/templates/assistkick-product-system/packages/frontend/src/components/ds/StatusDot.tsx +18 -0
  88. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCardPositionNode.tsx +36 -0
  89. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/CheckCycleCountNode.tsx +60 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/EndNode.tsx +42 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +189 -0
  92. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +123 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/RunAgentNode.tsx +51 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/SetCardMetadataNode.tsx +53 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/StartNode.tsx +18 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/TransitionCardNode.tsx +59 -0
  97. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +335 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +634 -0
  99. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/autoLayout.ts +103 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/edgeColors.ts +35 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +208 -0
  102. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.test.ts +119 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +107 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/constants/graph.ts +13 -11
  105. package/templates/assistkick-product-system/packages/frontend/src/hooks/useAutoSave.ts +75 -0
  106. package/templates/assistkick-product-system/packages/frontend/src/hooks/useToast.tsx +16 -3
  107. package/templates/assistkick-product-system/packages/frontend/src/pages/accept_invitation_page.tsx +30 -27
  108. package/templates/assistkick-product-system/packages/frontend/src/pages/forgot_password_page.tsx +18 -15
  109. package/templates/assistkick-product-system/packages/frontend/src/pages/register_page.tsx +21 -18
  110. package/templates/assistkick-product-system/packages/frontend/src/pages/reset_password_page.tsx +28 -25
  111. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +6 -0
  112. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +1 -1
  113. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +2 -2
  114. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +13 -0
  115. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -2
  116. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +13 -0
  117. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +6 -0
  118. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +6 -3
  119. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +4 -4
  120. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +275 -3535
  121. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.test.ts +167 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/utils/auto_save_service.ts +101 -0
  123. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.test.ts +42 -0
  124. package/templates/assistkick-product-system/packages/frontend/src/utils/composition_matcher.ts +17 -0
  125. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.test.ts +145 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/utils/file_utils.ts +42 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.test.ts +4 -10
  128. package/templates/assistkick-product-system/packages/frontend/src/utils/task_status.ts +19 -1
  129. package/templates/assistkick-product-system/packages/frontend/vite.config.ts +5 -0
  130. package/templates/assistkick-product-system/packages/shared/db/local.db +0 -0
  131. package/templates/assistkick-product-system/packages/shared/db/migrations/0004_tidy_matthew_murdock.sql +9 -0
  132. package/templates/assistkick-product-system/packages/shared/db/migrations/0005_mysterious_falcon.sql +692 -0
  133. package/templates/assistkick-product-system/packages/shared/db/migrations/0006_next_venom.sql +9 -0
  134. package/templates/assistkick-product-system/packages/shared/db/migrations/0007_deep_barracuda.sql +39 -0
  135. package/templates/assistkick-product-system/packages/shared/db/migrations/0008_puzzling_hannibal_king.sql +1 -0
  136. package/templates/assistkick-product-system/packages/shared/db/migrations/0009_amused_beast.sql +8 -0
  137. package/templates/assistkick-product-system/packages/shared/db/migrations/0010_spotty_moira_mactaggert.sql +9 -0
  138. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_goofy_snowbird.sql +3 -0
  139. package/templates/assistkick-product-system/packages/shared/db/migrations/0011_supreme_doctor_octopus.sql +3 -0
  140. package/templates/assistkick-product-system/packages/shared/db/migrations/0013_reflective_prowler.sql +15 -0
  141. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0004_snapshot.json +921 -0
  142. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0005_snapshot.json +1042 -0
  143. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0006_snapshot.json +1101 -0
  144. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0007_snapshot.json +1336 -0
  145. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0008_snapshot.json +1275 -0
  146. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0009_snapshot.json +1327 -0
  147. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0010_snapshot.json +1393 -0
  148. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0011_snapshot.json +1436 -0
  149. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0013_snapshot.json +1538 -0
  150. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
  151. package/templates/assistkick-product-system/packages/shared/db/schema.ts +113 -0
  152. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +32 -7
  153. package/templates/assistkick-product-system/packages/shared/lib/constants.ts +9 -0
  154. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +12 -4
  155. package/templates/assistkick-product-system/packages/shared/lib/graph.ts +5 -0
  156. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +1753 -0
  157. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +1281 -0
  158. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +211 -0
  159. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +43 -0
  160. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +13 -2
  161. package/templates/assistkick-product-system/packages/shared/tools/get_kanban.ts +1 -1
  162. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.test.ts +226 -0
  163. package/templates/assistkick-product-system/packages/shared/tools/migrate_epics.ts +251 -0
  164. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -2
  165. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.test.ts +10 -0
  166. package/templates/assistkick-product-system/packages/shared/utils/hello_workflow.ts +6 -0
  167. package/templates/assistkick-product-system/packages/video/Root.tsx +85 -0
  168. package/templates/assistkick-product-system/packages/video/components/email_scene.tsx +231 -0
  169. package/templates/assistkick-product-system/packages/video/components/outro_scene.tsx +153 -0
  170. package/templates/assistkick-product-system/packages/video/components/part_divider.tsx +90 -0
  171. package/templates/assistkick-product-system/packages/video/components/scene.tsx +226 -0
  172. package/templates/assistkick-product-system/packages/video/components/theme.ts +22 -0
  173. package/templates/assistkick-product-system/packages/video/components/title_scene.tsx +169 -0
  174. package/templates/assistkick-product-system/packages/video/components/video_split_layout.tsx +84 -0
  175. package/templates/assistkick-product-system/packages/video/compositions/.gitkeep +0 -0
  176. package/templates/assistkick-product-system/packages/video/index.ts +4 -0
  177. package/templates/assistkick-product-system/packages/video/package.json +28 -0
  178. package/templates/assistkick-product-system/packages/video/remotion.config.ts +11 -0
  179. package/templates/assistkick-product-system/packages/video/scripts/process_script.test.ts +326 -0
  180. package/templates/assistkick-product-system/packages/video/scripts/process_script.ts +630 -0
  181. package/templates/assistkick-product-system/packages/video/style.css +1 -0
  182. package/templates/assistkick-product-system/packages/video/tsconfig.json +18 -0
  183. package/templates/assistkick-product-system/tests/graph_legend.test.ts +2 -1
  184. package/templates/assistkick-product-system/tests/video_render_service.test.ts +179 -0
  185. package/templates/assistkick-product-system/tests/web_terminal.test.ts +219 -455
  186. package/templates/assistkick-product-system/tests/workflow_integration.test.ts +341 -0
  187. package/templates/skills/assistkick-developer/SKILL.md +3 -0
  188. package/templates/skills/assistkick-developer/references/react_development_guidelines.md +225 -0
  189. package/templates/skills/product-system/graph.json +1890 -0
  190. package/templates/skills/product-system/kanban.json +304 -0
  191. package/templates/skills/product-system/nodes/comp_001.md +56 -0
  192. package/templates/skills/product-system/nodes/comp_002.md +57 -0
  193. package/templates/skills/product-system/nodes/data_001.md +51 -0
  194. package/templates/skills/product-system/nodes/data_002.md +40 -0
  195. package/templates/skills/product-system/nodes/data_004.md +38 -0
  196. package/templates/skills/product-system/nodes/dec_001.md +34 -0
  197. package/templates/skills/product-system/nodes/dec_016.md +32 -0
  198. package/templates/skills/product-system/nodes/feat_008.md +30 -0
  199. package/templates/skills/video-composition-agent/SKILL.md +232 -0
  200. package/templates/skills/video-script-writer/SKILL.md +136 -0
@@ -0,0 +1,313 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
2
+ import {
3
+ Film,
4
+ Download,
5
+ Trash2,
6
+ Play,
7
+ X,
8
+ Clock,
9
+ HardDrive,
10
+ Loader2,
11
+ AlertCircle,
12
+ CheckCircle2,
13
+ CircleDot,
14
+ } from 'lucide-react';
15
+ import { apiClient } from '../api/client';
16
+ import type { VideoRenderStatus } from '../api/client';
17
+ import { IconButton } from './ds/IconButton';
18
+
19
+ const POLL_INTERVAL_MS = 3000;
20
+
21
+ interface VideoGalleryProps {
22
+ projectId: string;
23
+ }
24
+
25
+ const formatFileSize = (bytes: number | null): string => {
26
+ if (bytes === null || bytes === 0) return '—';
27
+ if (bytes < 1024) return `${bytes} B`;
28
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
29
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
30
+ };
31
+
32
+ const formatDuration = (seconds: number | null): string => {
33
+ if (seconds === null) return '—';
34
+ const m = Math.floor(seconds / 60);
35
+ const s = Math.round(seconds % 60);
36
+ return m > 0 ? `${m}m ${s}s` : `${s}s`;
37
+ };
38
+
39
+ const formatDate = (iso: string): string => {
40
+ const d = new Date(iso);
41
+ return d.toLocaleDateString(undefined, {
42
+ month: 'short',
43
+ day: 'numeric',
44
+ hour: '2-digit',
45
+ minute: '2-digit',
46
+ });
47
+ };
48
+
49
+ const statusConfig: Record<VideoRenderStatus['status'], { icon: React.ReactNode; label: string; color: string }> = {
50
+ queued: {
51
+ icon: <Clock size={12} />,
52
+ label: 'Queued',
53
+ color: 'text-content-muted',
54
+ },
55
+ rendering: {
56
+ icon: <Loader2 size={12} className="animate-spin" />,
57
+ label: 'Rendering',
58
+ color: 'text-amber-500',
59
+ },
60
+ complete: {
61
+ icon: <CheckCircle2 size={12} />,
62
+ label: 'Complete',
63
+ color: 'text-emerald-500',
64
+ },
65
+ failed: {
66
+ icon: <AlertCircle size={12} />,
67
+ label: 'Failed',
68
+ color: 'text-error',
69
+ },
70
+ };
71
+
72
+ export const VideoGallery = ({ projectId }: VideoGalleryProps) => {
73
+ const [renders, setRenders] = useState<VideoRenderStatus[]>([]);
74
+ const [loading, setLoading] = useState(true);
75
+ const [error, setError] = useState<string | null>(null);
76
+ const [playingId, setPlayingId] = useState<string | null>(null);
77
+ const [deletingId, setDeletingId] = useState<string | null>(null);
78
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
79
+
80
+ const fetchRenders = useCallback(async () => {
81
+ try {
82
+ const data = await apiClient.fetchVideoRenders(projectId);
83
+ setRenders(data.renders);
84
+ setError(null);
85
+ } catch (err: any) {
86
+ setError(err.message);
87
+ } finally {
88
+ setLoading(false);
89
+ }
90
+ }, [projectId]);
91
+
92
+ // Initial fetch
93
+ useEffect(() => {
94
+ setLoading(true);
95
+ fetchRenders();
96
+ }, [fetchRenders]);
97
+
98
+ // Poll when there are in-progress renders
99
+ const hasActiveRenders = renders.some(r => r.status === 'queued' || r.status === 'rendering');
100
+
101
+ useEffect(() => {
102
+ if (hasActiveRenders) {
103
+ pollRef.current = setInterval(fetchRenders, POLL_INTERVAL_MS);
104
+ }
105
+ return () => {
106
+ if (pollRef.current) {
107
+ clearInterval(pollRef.current);
108
+ pollRef.current = null;
109
+ }
110
+ };
111
+ }, [hasActiveRenders, fetchRenders]);
112
+
113
+ const handleDelete = useCallback(async (renderId: string) => {
114
+ setDeletingId(renderId);
115
+ try {
116
+ await apiClient.deleteVideoRender(renderId);
117
+ setRenders(prev => prev.filter(r => r.id !== renderId));
118
+ if (playingId === renderId) setPlayingId(null);
119
+ } catch (err: any) {
120
+ setError(err.message);
121
+ } finally {
122
+ setDeletingId(null);
123
+ }
124
+ }, [playingId]);
125
+
126
+ const handleDownload = useCallback((renderId: string) => {
127
+ const url = apiClient.getVideoFileUrl(renderId);
128
+ const a = document.createElement('a');
129
+ a.href = url;
130
+ a.download = `${renderId}.mp4`;
131
+ document.body.appendChild(a);
132
+ a.click();
133
+ document.body.removeChild(a);
134
+ }, []);
135
+
136
+ if (loading) {
137
+ return (
138
+ <div className="flex items-center justify-center py-8 text-content-muted">
139
+ <Loader2 size={20} className="animate-spin mr-2" />
140
+ <span className="text-sm">Loading renders...</span>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ if (error && renders.length === 0) {
146
+ return (
147
+ <div className="flex flex-col items-center gap-2 py-8 text-content-muted">
148
+ <AlertCircle size={20} />
149
+ <span className="text-sm">{error}</span>
150
+ <button
151
+ onClick={fetchRenders}
152
+ className="mt-1 px-3 py-1.5 rounded-md border border-edge bg-surface text-xs text-content hover:bg-surface-hover transition-colors"
153
+ >
154
+ Retry
155
+ </button>
156
+ </div>
157
+ );
158
+ }
159
+
160
+ if (renders.length === 0) {
161
+ return (
162
+ <div className="flex flex-col items-center gap-2 py-8 text-content-muted">
163
+ <Film size={32} strokeWidth={1} />
164
+ <span className="text-sm font-medium">No rendered videos yet</span>
165
+ <span className="text-xs text-center max-w-xs">
166
+ Start a render from the workflow pipeline to see your videos here.
167
+ </span>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ return (
173
+ <div className="flex flex-col gap-2">
174
+ {error && (
175
+ <div className="text-xs text-error px-2 py-1 rounded bg-error/10 border border-error/20">
176
+ {error}
177
+ </div>
178
+ )}
179
+
180
+ {renders.map(render => {
181
+ const cfg = statusConfig[render.status];
182
+ const isPlaying = playingId === render.id;
183
+ const isDeleting = deletingId === render.id;
184
+ const isComplete = render.status === 'complete';
185
+ const isActive = render.status === 'queued' || render.status === 'rendering';
186
+
187
+ return (
188
+ <div
189
+ key={render.id}
190
+ className="rounded-lg border border-edge bg-surface p-3 transition-colors hover:bg-surface-alt"
191
+ >
192
+ {/* Inline video player */}
193
+ {isPlaying && isComplete && (
194
+ <div className="mb-3 rounded-md overflow-hidden bg-black relative">
195
+ <video
196
+ src={apiClient.getVideoFileUrl(render.id)}
197
+ controls
198
+ autoPlay
199
+ className="w-full max-h-[400px]"
200
+ />
201
+ <button
202
+ onClick={() => setPlayingId(null)}
203
+ className="absolute top-2 right-2 flex h-7 w-7 items-center justify-center rounded-full bg-black/60 text-white hover:bg-black/80 transition-colors"
204
+ >
205
+ <X size={14} />
206
+ </button>
207
+ </div>
208
+ )}
209
+
210
+ <div className="flex items-center justify-between gap-3">
211
+ {/* Left side: status + info */}
212
+ <div className="flex items-center gap-3 min-w-0 flex-1">
213
+ {/* Status icon */}
214
+ <div className={`flex items-center gap-1.5 shrink-0 ${cfg.color}`}>
215
+ {cfg.icon}
216
+ <span className="text-[11px] font-medium">{cfg.label}</span>
217
+ </div>
218
+
219
+ {/* Composition name */}
220
+ <span className="text-[12px] font-mono text-content truncate">
221
+ {render.compositionId}
222
+ </span>
223
+
224
+ {/* Progress for active renders */}
225
+ {isActive && (
226
+ <div className="flex items-center gap-2 shrink-0">
227
+ <div className="w-20 h-1.5 rounded-full bg-surface-alt overflow-hidden">
228
+ <div
229
+ className="h-full rounded-full bg-amber-500 transition-all duration-300"
230
+ style={{ width: `${render.progress}%` }}
231
+ />
232
+ </div>
233
+ <span className="text-[11px] text-content-muted">{render.progress}%</span>
234
+ </div>
235
+ )}
236
+
237
+ {/* Metadata for completed renders */}
238
+ {isComplete && (
239
+ <div className="flex items-center gap-3 text-[11px] text-content-muted shrink-0">
240
+ {render.resolution && (
241
+ <span className="flex items-center gap-1">
242
+ <CircleDot size={10} />
243
+ {render.resolution}
244
+ </span>
245
+ )}
246
+ {render.durationSeconds !== null && (
247
+ <span className="flex items-center gap-1">
248
+ <Clock size={10} />
249
+ {formatDuration(render.durationSeconds)}
250
+ </span>
251
+ )}
252
+ {render.fileSize !== null && (
253
+ <span className="flex items-center gap-1">
254
+ <HardDrive size={10} />
255
+ {formatFileSize(render.fileSize)}
256
+ </span>
257
+ )}
258
+ </div>
259
+ )}
260
+
261
+ {/* Error message */}
262
+ {render.status === 'failed' && render.error && (
263
+ <span className="text-[11px] text-error truncate" title={render.error}>
264
+ {render.error}
265
+ </span>
266
+ )}
267
+ </div>
268
+
269
+ {/* Right side: date + actions */}
270
+ <div className="flex items-center gap-2 shrink-0">
271
+ <span className="text-[11px] text-content-muted">
272
+ {formatDate(render.createdAt)}
273
+ </span>
274
+
275
+ {isComplete && (
276
+ <>
277
+ <IconButton
278
+ variant="ghost"
279
+ size="sm"
280
+ label={isPlaying ? 'Stop playback' : 'Play video'}
281
+ onClick={() => setPlayingId(isPlaying ? null : render.id)}
282
+ >
283
+ {isPlaying ? <X size={13} /> : <Play size={13} />}
284
+ </IconButton>
285
+
286
+ <IconButton
287
+ variant="ghost"
288
+ size="sm"
289
+ label="Download MP4"
290
+ onClick={() => handleDownload(render.id)}
291
+ >
292
+ <Download size={13} />
293
+ </IconButton>
294
+ </>
295
+ )}
296
+
297
+ <IconButton
298
+ variant="danger"
299
+ size="sm"
300
+ label="Delete render"
301
+ disabled={isDeleting}
302
+ onClick={() => handleDelete(render.id)}
303
+ >
304
+ {isDeleting ? <Loader2 size={13} className="animate-spin" /> : <Trash2 size={13} />}
305
+ </IconButton>
306
+ </div>
307
+ </div>
308
+ </div>
309
+ );
310
+ })}
311
+ </div>
312
+ );
313
+ };
@@ -0,0 +1,250 @@
1
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { Player } from '@remotion/player';
3
+ import { ChevronDown, RefreshCw, Video, AlertCircle, Film, Library } from 'lucide-react';
4
+ import { apiClient } from '../api/client';
5
+ import type { VideoCompositionMeta, VideoBundleStatus } from '../api/client';
6
+ import { CompositionPlaceholder } from './CompositionPlaceholder';
7
+ import { VideoGallery } from './VideoGallery';
8
+ import { findCompositionModuleKey } from '../utils/composition_matcher';
9
+
10
+ /**
11
+ * Discover composition modules from the video package at build time.
12
+ * Each matching file becomes a lazy-loaded chunk via Vite's code splitting.
13
+ * The Remotion Player requires the same React instance as the host page,
14
+ * so compositions must be loaded through the build system rather than
15
+ * fetched as standalone scripts from a separate bundle.
16
+ *
17
+ * When the Video Composition Agent creates a new composition and it's merged,
18
+ * the auto-rebuild produces both the Remotion bundle (for server-side rendering)
19
+ * and the frontend build (picking up new compositions via this glob).
20
+ */
21
+ const compositionModules = import.meta.glob<{
22
+ component?: React.FC<Record<string, unknown>>;
23
+ default?: React.FC<Record<string, unknown>>;
24
+ }>('../../../video/compositions/*/index.tsx');
25
+
26
+ interface VideographyViewProps {
27
+ projectId: string | null;
28
+ }
29
+
30
+ export const VideographyView = ({ projectId }: VideographyViewProps) => {
31
+ const [compositions, setCompositions] = useState<VideoCompositionMeta[]>([]);
32
+ const [selectedId, setSelectedId] = useState<string | null>(null);
33
+ const [bundleStatus, setBundleStatus] = useState<VideoBundleStatus | null>(null);
34
+ const [loading, setLoading] = useState(true);
35
+ const [error, setError] = useState<string | null>(null);
36
+ const [rebuilding, setRebuilding] = useState(false);
37
+
38
+ const fetchData = useCallback(async () => {
39
+ setLoading(true);
40
+ setError(null);
41
+ try {
42
+ const [compositionsData, statusData] = await Promise.all([
43
+ apiClient.fetchVideoCompositions(),
44
+ apiClient.fetchVideoBundleStatus(),
45
+ ]);
46
+ setCompositions(compositionsData.compositions);
47
+ setBundleStatus(statusData);
48
+ if (compositionsData.compositions.length > 0 && !selectedId) {
49
+ setSelectedId(compositionsData.compositions[0].id);
50
+ }
51
+ } catch (err: any) {
52
+ setError(err.message);
53
+ } finally {
54
+ setLoading(false);
55
+ }
56
+ }, [selectedId]);
57
+
58
+ useEffect(() => {
59
+ fetchData();
60
+ }, [projectId]);
61
+
62
+ const handleRebuild = useCallback(async () => {
63
+ setRebuilding(true);
64
+ try {
65
+ const status = await apiClient.rebuildVideoBundle();
66
+ setBundleStatus(status);
67
+ await fetchData();
68
+ } catch (err: any) {
69
+ setError(err.message);
70
+ } finally {
71
+ setRebuilding(false);
72
+ }
73
+ }, [fetchData]);
74
+
75
+ const selected = compositions.find(c => c.id === selectedId);
76
+
77
+ /**
78
+ * Create a stable lazyComponent loader for the Remotion Player.
79
+ * Loads the actual composition from the video package when available,
80
+ * falls back to CompositionPlaceholder otherwise.
81
+ */
82
+ const compositionLoader = useMemo(() => {
83
+ if (!selectedId || !selected) {
84
+ return () => Promise.resolve({
85
+ default: CompositionPlaceholder as React.FC<Record<string, unknown>>,
86
+ });
87
+ }
88
+
89
+ // Match composition module by directory name (equals composition ID by convention)
90
+ const moduleKey = findCompositionModuleKey(Object.keys(compositionModules), selectedId);
91
+
92
+ if (moduleKey) {
93
+ return async () => {
94
+ try {
95
+ const mod = await compositionModules[moduleKey]();
96
+ const comp = mod.component || mod.default;
97
+ if (comp) return { default: comp };
98
+ } catch {
99
+ // Composition failed to load — fall through to placeholder
100
+ }
101
+ return {
102
+ default: CompositionPlaceholder as React.FC<Record<string, unknown>>,
103
+ };
104
+ };
105
+ }
106
+
107
+ // Composition exists in API but not in the build — use placeholder
108
+ return () => Promise.resolve({
109
+ default: CompositionPlaceholder as React.FC<Record<string, unknown>>,
110
+ });
111
+ }, [selectedId, selected]);
112
+
113
+ if (loading) {
114
+ return (
115
+ <div className="flex h-full items-center justify-center">
116
+ <div className="flex flex-col items-center gap-3 text-content-muted">
117
+ <Film size={32} strokeWidth={1.5} className="animate-pulse" />
118
+ <span className="text-sm font-medium">Loading compositions...</span>
119
+ </div>
120
+ </div>
121
+ );
122
+ }
123
+
124
+ if (error) {
125
+ return (
126
+ <div className="flex h-full items-center justify-center">
127
+ <div className="flex flex-col items-center gap-3 text-content-muted">
128
+ <AlertCircle size={32} strokeWidth={1.5} />
129
+ <span className="text-sm font-medium">Failed to load video data</span>
130
+ <span className="text-xs">{error}</span>
131
+ <button
132
+ onClick={fetchData}
133
+ className="mt-2 px-3 py-1.5 rounded-md border border-edge bg-surface text-xs text-content hover:bg-surface-hover transition-colors"
134
+ >
135
+ Retry
136
+ </button>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
141
+
142
+ return (
143
+ <div className="flex flex-col h-full w-full overflow-y-auto p-4 gap-4">
144
+ {/* Header bar */}
145
+ <div className="flex items-center justify-between gap-4 shrink-0">
146
+ <div className="flex items-center gap-3">
147
+ <Video size={18} strokeWidth={1.5} className="text-content-muted" />
148
+ <h2 className="text-sm font-semibold text-content">Remotion Preview</h2>
149
+ </div>
150
+
151
+ <div className="flex items-center gap-3">
152
+ {/* Composition selector */}
153
+ {compositions.length > 0 && (
154
+ <div className="relative">
155
+ <select
156
+ value={selectedId || ''}
157
+ onChange={(e) => setSelectedId(e.target.value)}
158
+ className="px-3 py-1.5 rounded-md border border-edge bg-surface text-[12px] text-content appearance-none pr-7 min-w-[200px]"
159
+ >
160
+ {compositions.map(c => (
161
+ <option key={c.id} value={c.id}>
162
+ {c.id} ({c.width}x{c.height} @ {c.fps}fps)
163
+ </option>
164
+ ))}
165
+ </select>
166
+ <ChevronDown size={12} className="absolute right-2 top-1/2 -translate-y-1/2 text-content-muted pointer-events-none" />
167
+ </div>
168
+ )}
169
+
170
+ {/* Bundle status + rebuild */}
171
+ <div className="flex items-center gap-2">
172
+ {bundleStatus && (
173
+ <span className="text-[11px] text-content-muted">
174
+ {bundleStatus.ready ? 'Bundle ready' : 'No bundle'}
175
+ {bundleStatus.lastBuiltAt && (
176
+ <> &middot; {new Date(bundleStatus.lastBuiltAt).toLocaleString()}</>
177
+ )}
178
+ </span>
179
+ )}
180
+ <button
181
+ onClick={handleRebuild}
182
+ disabled={rebuilding}
183
+ title="Rebuild Remotion bundle"
184
+ className="flex h-7 w-7 items-center justify-center rounded-md border border-edge text-content-muted hover:text-content hover:border-content/20 transition-all disabled:opacity-50"
185
+ >
186
+ <RefreshCw size={13} strokeWidth={2} className={rebuilding ? 'animate-spin' : ''} />
187
+ </button>
188
+ </div>
189
+ </div>
190
+ </div>
191
+
192
+ {/* Player area */}
193
+ <div className="flex-1 flex items-center justify-center rounded-lg border border-edge bg-black/5 dark:bg-white/5 overflow-hidden min-h-0">
194
+ {compositions.length === 0 ? (
195
+ <div className="flex flex-col items-center gap-3 text-content-muted">
196
+ <Video size={48} strokeWidth={1} />
197
+ <span className="text-sm font-medium">No compositions available</span>
198
+ <span className="text-xs text-center max-w-sm">
199
+ Compositions are created by the Video Composition Agent.
200
+ Run the agent workflow to generate your first composition.
201
+ </span>
202
+ </div>
203
+ ) : selected ? (
204
+ <div className="w-full h-full flex items-center justify-center p-4">
205
+ <Player
206
+ lazyComponent={compositionLoader}
207
+ inputProps={{
208
+ compositionId: selected.id,
209
+ width: selected.width,
210
+ height: selected.height,
211
+ }}
212
+ durationInFrames={selected.durationInFrames}
213
+ compositionWidth={selected.width}
214
+ compositionHeight={selected.height}
215
+ fps={selected.fps}
216
+ controls
217
+ style={{
218
+ width: '100%',
219
+ maxHeight: '100%',
220
+ aspectRatio: `${selected.width} / ${selected.height}`,
221
+ }}
222
+ />
223
+ </div>
224
+ ) : null}
225
+ </div>
226
+
227
+ {/* Composition metadata */}
228
+ {selected && (
229
+ <div className="flex items-center gap-4 text-[11px] text-content-muted shrink-0">
230
+ <span>ID: <span className="font-mono text-content">{selected.id}</span></span>
231
+ <span>Resolution: <span className="font-mono text-content">{selected.width}x{selected.height}</span></span>
232
+ <span>FPS: <span className="font-mono text-content">{selected.fps}</span></span>
233
+ <span>Duration: <span className="font-mono text-content">{selected.durationInFrames} frames ({(selected.durationInFrames / selected.fps).toFixed(1)}s)</span></span>
234
+ {selected.folder && <span>Folder: <span className="font-mono text-content">{selected.folder}</span></span>}
235
+ </div>
236
+ )}
237
+
238
+ {/* Video Gallery */}
239
+ {projectId && (
240
+ <div className="flex flex-col gap-3 shrink-0">
241
+ <div className="flex items-center gap-2">
242
+ <Library size={16} strokeWidth={1.5} className="text-content-muted" />
243
+ <h3 className="text-sm font-semibold text-content">Rendered Videos</h3>
244
+ </div>
245
+ <VideoGallery projectId={projectId} />
246
+ </div>
247
+ )}
248
+ </div>
249
+ );
250
+ };