diff --git a/app/src/components/StoriesTab/StoryChatItem.tsx b/app/src/components/StoriesTab/StoryChatItem.tsx index 8ae49b0d..dbc823f4 100644 --- a/app/src/components/StoriesTab/StoryChatItem.tsx +++ b/app/src/components/StoriesTab/StoryChatItem.tsx @@ -1,6 +1,6 @@ import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { GripVertical, Mic, MoreHorizontal, Play, Trash2 } from 'lucide-react'; +import { GripVertical, Mic, MoreHorizontal, Play, RotateCcw, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/button'; @@ -21,6 +21,7 @@ interface StoryChatItemProps { storyId: string; index: number; onRemove: () => void; + onRegenerate?: () => void; currentTimeMs: number; isPlaying: boolean; dragHandleProps?: React.HTMLAttributes; @@ -30,6 +31,7 @@ interface StoryChatItemProps { export function StoryChatItem({ item, onRemove, + onRegenerate, currentTimeMs, isPlaying, dragHandleProps, @@ -134,6 +136,12 @@ export function StoryChatItem({ {t('storyContent.itemActions.playFromHere')} + {onRegenerate && ( + + + {t('storyContent.itemActions.regenerate')} + + )} (null); const pendingCount = useGenerationStore((s) => s.pendingGenerationIds.size); + const addPendingGeneration = useGenerationStore((s) => s.addPendingGeneration); // Add generation popover state const [searchQuery, setSearchQuery] = useState(''); @@ -142,6 +144,19 @@ export function StoryContent() { } }, [isPlaying]); + const handleRegenerate = async (generationId: string) => { + try { + await apiClient.regenerateGeneration(generationId); + addPendingGeneration(generationId); + } catch (error) { + toast({ + title: t('storyContent.toast.regenerateFailed'), + description: error instanceof Error ? error.message : String(error), + variant: 'destructive', + }); + } + }; + const handleRemoveItem = (itemId: string) => { if (!story) return; @@ -399,6 +414,7 @@ export function StoryContent() { storyId={story.id} index={index} onRemove={() => handleRemoveItem(item.id)} + onRegenerate={() => handleRegenerate(item.generation_id)} currentTimeMs={currentTimeMs} isPlaying={isPlaying && playbackStoryId === story.id} /> diff --git a/app/src/components/StoriesTab/StoryTrackEditor.tsx b/app/src/components/StoriesTab/StoryTrackEditor.tsx index d0afe8e3..41e9cea7 100644 --- a/app/src/components/StoriesTab/StoryTrackEditor.tsx +++ b/app/src/components/StoriesTab/StoryTrackEditor.tsx @@ -7,6 +7,7 @@ import { Pause, Play, Plus, + RotateCcw, Scissors, Square, Trash2, @@ -32,6 +33,7 @@ import { useTrimStoryItem, } from '@/lib/hooks/useStories'; import { cn } from '@/lib/utils/cn'; +import { useGenerationStore } from '@/stores/generationStore'; import { useStoryStore } from '@/stores/storyStore'; // Clip waveform component with trim support @@ -152,6 +154,7 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) { const removeItem = useRemoveStoryItem(); const setItemVersion = useSetStoryItemVersion(); const { toast } = useToast(); + const addPendingGeneration = useGenerationStore((s) => s.addPendingGeneration); // Selection state const selectedClipId = useStoryStore((state) => state.selectedClipId); @@ -630,6 +633,20 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) { ); }, [selectedClipId, storyId, removeItem, toast, setSelectedClipId]); + const handleRegenerate = useCallback(async () => { + if (!selectedItem) return; + try { + await apiClient.regenerateGeneration(selectedItem.generation_id); + addPendingGeneration(selectedItem.generation_id); + } catch (error) { + toast({ + title: 'Failed to regenerate', + description: error instanceof Error ? error.message : String(error), + variant: 'destructive', + }); + } + }, [selectedItem, addPendingGeneration, toast]); + // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -999,6 +1016,16 @@ export function StoryTrackEditor({ storyId, items }: StoryTrackEditorProps) { > + {hasMultipleVersions && ( <>
diff --git a/app/src/i18n/locales/en/translation.json b/app/src/i18n/locales/en/translation.json index 5eafe63f..3137cee5 100644 --- a/app/src/i18n/locales/en/translation.json +++ b/app/src/i18n/locales/en/translation.json @@ -629,13 +629,15 @@ }, "itemActions": { "playFromHere": "Play from here", + "regenerate": "Regenerate", "removeFromStory": "Remove from Story" }, "toast": { "removeFailed": "Failed to remove item", "reorderFailed": "Failed to reorder items", "exportFailed": "Failed to export audio", - "addFailed": "Failed to add generation" + "addFailed": "Failed to add generation", + "regenerateFailed": "Failed to regenerate" } }, "history": { diff --git a/app/src/i18n/locales/ja/translation.json b/app/src/i18n/locales/ja/translation.json index fffe000e..43da07e3 100644 --- a/app/src/i18n/locales/ja/translation.json +++ b/app/src/i18n/locales/ja/translation.json @@ -629,13 +629,15 @@ }, "itemActions": { "playFromHere": "ここから再生", + "regenerate": "再生成", "removeFromStory": "ストーリーから削除" }, "toast": { "removeFailed": "項目の削除に失敗しました", "reorderFailed": "項目の並び替えに失敗しました", "exportFailed": "オーディオのエクスポートに失敗しました", - "addFailed": "生成の追加に失敗しました" + "addFailed": "生成の追加に失敗しました", + "regenerateFailed": "再生成に失敗しました" } }, "history": { diff --git a/app/src/i18n/locales/zh-CN/translation.json b/app/src/i18n/locales/zh-CN/translation.json index a069c8a7..ec70d8cc 100644 --- a/app/src/i18n/locales/zh-CN/translation.json +++ b/app/src/i18n/locales/zh-CN/translation.json @@ -629,13 +629,15 @@ }, "itemActions": { "playFromHere": "从此处播放", + "regenerate": "重新生成", "removeFromStory": "从故事中移除" }, "toast": { "removeFailed": "移除项目失败", "reorderFailed": "重新排序项目失败", "exportFailed": "导出音频失败", - "addFailed": "添加生成失败" + "addFailed": "添加生成失败", + "regenerateFailed": "重新生成失败" } }, "history": { diff --git a/app/src/i18n/locales/zh-TW/translation.json b/app/src/i18n/locales/zh-TW/translation.json index 11155099..78fafd5a 100644 --- a/app/src/i18n/locales/zh-TW/translation.json +++ b/app/src/i18n/locales/zh-TW/translation.json @@ -629,13 +629,15 @@ }, "itemActions": { "playFromHere": "從此處播放", + "regenerate": "重新生成", "removeFromStory": "從故事中移除" }, "toast": { "removeFailed": "移除項目失敗", "reorderFailed": "重新排序項目失敗", "exportFailed": "匯出音訊失敗", - "addFailed": "新增生成失敗" + "addFailed": "新增生成失敗", + "regenerateFailed": "重新生成失敗" } }, "history": {