Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@ app.whenReady().then(() => {
console.log('[FRONTEND LOG]', ...args)
})

ipcMain.handle('start-pipeline', async (event, { threadId, newAiMessageId, isThinkingMode }) => {
console.log(`\n===============\n[DEBUG IPC] start-pipeline called: threadId=${threadId}, newAiMessageId=${newAiMessageId}, isThinkingMode=${isThinkingMode}`)
ipcMain.handle('start-pipeline', async (event, { threadId, newAiMessageId, isThinkingMode, autoUseImages }) => {
console.log(`\n===============\n[DEBUG IPC] start-pipeline called: threadId=${threadId}, newAiMessageId=${newAiMessageId}, isThinkingMode=${isThinkingMode}, autoUseImages=${autoUseImages}`)
const window = BrowserWindow.fromWebContents(event.sender)
if (!window) {
console.log(`[DEBUG IPC] FAILED: window not found`)
Expand All @@ -206,7 +206,7 @@ app.whenReady().then(() => {
// Prepare the base timeline
const baseTimeline = userMsgId ? thread.messages.find(m => m.id === userMsgId)?.timeline : undefined;

const pipeline = new Pipeline(window, newAiMessageId, threadId, context, baseTimeline, userMsgId, attachedImages, isThinkingMode)
const pipeline = new Pipeline(window, newAiMessageId, threadId, context, baseTimeline, userMsgId, attachedImages, isThinkingMode, autoUseImages)

// Ensure background processing is running (will resume/retry if tasks are missing/failed)
if (thread.type === 'image') {
Expand Down
6 changes: 5 additions & 1 deletion src/main/pipeline/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface PipelineContext {
intentResult?: import('../../shared/types').IntentResult;
attachedImages?: string[];
isThinkingMode?: boolean;
autoUseImages?: boolean;
}

import { backgroundTaskManager } from '../tasks'
Expand All @@ -41,10 +42,11 @@ export class Pipeline {
private intentResult?: import('../../shared/types').IntentResult
private attachedImages?: string[]
private isThinkingMode: boolean = false
private autoUseImages: boolean = false
private isFinished: boolean = false
private abortController: AbortController = new AbortController()

constructor(browserWindow: BrowserWindow, messageId: string, threadId: string, context: string, baseTimeline?: EnrichedTimelineSegment[], editRefId?: string, attachedImages?: string[], isThinkingMode: boolean = false) {
constructor(browserWindow: BrowserWindow, messageId: string, threadId: string, context: string, baseTimeline?: EnrichedTimelineSegment[], editRefId?: string, attachedImages?: string[], isThinkingMode: boolean = false, autoUseImages: boolean = false) {
this.browserWindow = browserWindow
this.messageId = messageId
this.threadId = threadId
Expand All @@ -53,6 +55,7 @@ export class Pipeline {
this.editRefId = editRefId
this.attachedImages = attachedImages
this.isThinkingMode = isThinkingMode
this.autoUseImages = autoUseImages
}

register(fn: PipelineFunction, options?: { skipIf?: (context: PipelineContext) => boolean }): this {
Expand Down Expand Up @@ -150,6 +153,7 @@ export class Pipeline {
baseTimeline: this.baseTimeline,
attachedImages: this.attachedImages,
isThinkingMode: this.isThinkingMode,
autoUseImages: this.autoUseImages,
get intentResult() { return self.intentResult },
set intentResult(val) { self.intentResult = val },
signal: this.abortController.signal,
Expand Down
5 changes: 5 additions & 0 deletions src/main/pipeline/phases/image-intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Rules (STRICT ENFORCEMENT):
b) Create a DETAILED technical prompt for a creative image generator (like Gemini Image 3).
c) This prompt should specify style, composition, lighting, and how to merge the elements from the selected images AND any provided ATTACHED IMAGES.
d) PERSON NAMES: DO NOT mention specific real-world names (e.g., 'Olga Loiek') in the 'content' field. Instead, refer to them using generic descriptors based on the images, such as 'the speaker', 'the subject', 'the person in the video', or 'the main figure'. This is to avoid triggering safety/privacy filters. You can refer to 'Image X' or 'Image index Y' to point to specific people.
e) IMAGE ACCESS RESTRICTION: If the system note indicates "IMAGE ACCESS DISABLED", you CANNOT return 'selectedIndices' or trigger visual generation actions that require specific project images. In this case, use 'type': 'text' and ask the user to enable 'Smart Auto-References' (the blade icon).

Respond ONLY with a JSON object following this schema:
{
Expand Down Expand Up @@ -82,6 +83,10 @@ ${context.context}
User Prompt:
${lastUserPrompt}

${context.autoUseImages || (context.attachedImages && context.attachedImages.length > 0)
? "[SYSTEM NOTE]: IMAGE ACCESS IS ENABLED. You can select project images as references."
: "[SYSTEM NOTE]: IMAGE ACCESS IS DISABLED. You can see visual descriptions but cannot access actual image files. To perform visual tasks, ask the user to enable 'Smart Auto-References'."}

(Note: If the user provided any attached images, they are passed as visual context to you.)
`
console.log(`[IMAGE-INTENT] Sending prompt to AI. User prompt detected as: "${lastUserPrompt}"`)
Expand Down
7 changes: 6 additions & 1 deletion src/main/pipeline/phases/intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ Specific rules for 'content' field:
- If type is 'generate-timeline': This is a COMPREHENSIVE and DETAILED technical description for the timeline builder agent.
- If type is 'generate-thumbnail': This is a COMPREHENSIVE and DETAILED technical description for the thumbnail generator. Include visual elements, frames to extract (YOU MUST list specific timestamps in [HH:MM:SS] format for at least 2-3 relevant scenes), and any overlay text.
- CRITICAL: When editing an existing timeline or thumbnail, specify EXACTLY which parts to keep, remove, or replace. The goal is maximum consistency with the REFERENCE TIMELINE except for the requested changes. It will NOT be shown to the user. 'content' for 'generate-thumbnail' MUST be technical and precise.
- PERSON NAMES: DO NOT mention specific real-world names (e.g., 'Olga Loiek') in the 'content' field for 'generate-thumbnail'. Instead, refer to them using generic descriptors based on the reference frames, such as 'the speaker', 'the subject', 'the person in the video', or 'the main figure'. This is to avoid triggering safety/privacy filters. You can refer to 'Scene X' or 'Image Y' to point to specific people.`
- PERSON NAMES: DO NOT mention specific real-world names (e.g., 'Olga Loiek') in the 'content' field for 'generate-thumbnail'. Instead, refer to them using generic descriptors based on the reference frames, such as 'the speaker', 'the subject', 'the person in the video', or 'the main figure'. This is to avoid triggering safety/privacy filters. You can refer to 'Scene X' or 'Image Y' to point to specific people.
- IMAGE ACCESS RESTRICTION: If the system note indicates "IMAGE ACCESS DISABLED", you CANNOT return 'selectedIndices' or trigger visual generation actions that require specific project frames. In this case, use 'type': 'text' and ask the user to enable 'Smart Auto-References' (the blade icon).`

const INTENT_SCHEMA = {
type: 'object',
Expand Down Expand Up @@ -116,6 +117,10 @@ ${baseTimelineContext}

Conversation History:
${context.context}

${context.autoUseImages || (context.attachedImages && context.attachedImages.length > 0)
? "[SYSTEM NOTE]: IMAGE ACCESS IS ENABLED. You can use project images as references and return 'selectedIndices'."
: "[SYSTEM NOTE]: IMAGE ACCESS IS DISABLED. You can see visual descriptions but cannot access actual image files. To perform visual tasks, ask the user to enable 'Smart Auto-References'."}
`

// 5. Limit and Deduplicate Images (Base64 is expensive, limit to last 8)
Expand Down
11 changes: 10 additions & 1 deletion src/main/pipeline/phases/supply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,24 @@ import fs from 'fs'
export const supplyController: PipelineFunction = async (data, context) => {
context.updateStatus('Managing reference images...')

// Determine if we can use project images
const isImageAccessEnabled = context.autoUseImages || (context.attachedImages && context.attachedImages.length > 0)

// Case 1: User provided attachments
// Rule: If user provide frames, then none for auto select.
if (context.attachedImages && context.attachedImages.length > 0) {
console.log(`[SUPPLY] User provided ${context.attachedImages.length} attachments. Skipping auto-selection from video.`)
const cleanedPaths = context.attachedImages.map(url => url.replace(/^media:\/+/i, '/'))
context.next({ ...data, selectedReferenceImages: cleanedPaths })
return
}

// Case 2: No attachments, handle auto-selection ONLY if image access is enabled
if (!isImageAccessEnabled) {
console.log('[SUPPLY] Image access disabled and no attachments. Providing empty image list.')
context.next({ ...data, selectedReferenceImages: [] })
return
}

// Case 2: No attachments, handle auto-selection from video frames or source images
const referenceFrames = context.preprocessing?.['reference-frames'] || []
const sourceImages = context.preprocessing?.['sourceImages'] || []
Expand Down
112 changes: 72 additions & 40 deletions src/renderer/src/components/chat/BaseMessageInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,53 +21,79 @@
</div>

<!-- Main Input area -->
<div class="flex items-end transition-all duration-300" :class="[
<div class="flex flex-col transition-all duration-300" :class="[
compact
? 'space-x-1.5'
: 'space-x-4 p-1.5 rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-white/50 dark:bg-zinc-950/20 backdrop-blur-xl shadow-lg focus-within:ring-2 focus-within:ring-primary/20'
? 'space-y-1 bg-zinc-50/50 dark:bg-white/[0.02] p-2 rounded-xl border border-black/5 dark:border-white/5'
: 'space-y-4 p-4 rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-white/50 dark:bg-zinc-950/20 backdrop-blur-xl shadow-lg focus-within:ring-2 focus-within:ring-primary/20'
]">
<!-- Attachment Toggle -->
<SlimTooltip text="Add Attachments" :placement="compact ? 'top' : 'bottom'">
<IconButton @click="showAttachmentModal = true" icon="IconPlus" :size="compact ? 'sm' : 'md'" rounded="full"
variant="ghost" class="mb-0.5 text-zinc-400 hover:text-primary transition-all active:scale-95" />
</SlimTooltip>

<!-- Row 1: Textarea -->
<div class="flex-1 min-w-0">
<textarea v-model="internalText" ref="textareaRef" :placeholder="placeholder" :rows="1"
class="w-full bg-transparent border-none focus:ring-0 focus:outline-none text-zinc-800 dark:text-zinc-200 placeholder:text-zinc-400 resize-none py-2 px-1 leading-relaxed custom-scrollbar"
:class="compact ? 'text-[11px] max-h-[150px]' : 'text-sm max-h-[300px]'" @input="adjustTextarea"
@keydown.enter="handleEnter"></textarea>
</div>

<!-- Results Count -->
<div
class="flex items-center bg-zinc-100 dark:bg-white/5 rounded-xl px-2.5 py-1.5 mb-1 border border-black/5 dark:border-white/10 group/count hover:border-primary/30 transition-colors"
:class="compact ? 'scale-90 origin-right' : ''">
<span
class="text-[9px] font-black uppercase tracking-widest text-zinc-400 group-hover/count:text-primary/70 transition-colors mr-1.5 select-none">Count</span>
<select v-model="resultsCount"
class="bg-transparent border-none text-[11px] font-black text-zinc-700 dark:text-zinc-300 focus:ring-0 p-0 pr-4 cursor-pointer hover:text-primary transition-colors">
<option v-for="n in 5" :key="n" :value="n">{{ n }}</option>
</select>
</div>
<!-- Row 2: Actions -->
<div class="flex items-center justify-between gap-2 border-t border-black/5 dark:border-white/5 pt-1.5 mt-1">
<div class="flex items-center gap-1.5">
<!-- Attachment Toggle -->
<SlimTooltip text="Add Attachments" :placement="compact ? 'top' : 'bottom'">
<IconButton @click="showAttachmentModal = true" icon="IconPlus" :size="compact ? 'sm' : 'md'" rounded="full"
variant="ghost" class="text-zinc-400 hover:text-primary transition-all active:scale-95" />
</SlimTooltip>

<!-- Auto Use Images Toggle -->
<SlimTooltip text="Smart Auto-References: Include project images as context" :placement="compact ? 'top' : 'bottom'">
<button
@click="toggleAutoUseImages"
type="button"
class="flex items-center justify-center p-1.5 rounded-lg border transition-all active:scale-95"
:class="[
props.autoUseImages
? 'bg-primary/10 border-primary/30 text-primary shadow-sm shadow-primary/10'
: 'bg-zinc-100 dark:bg-white/5 border-black/5 dark:border-white/10 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300'
]"
>
<Icon name="iconify tabler--blade-filled" class="w-3.5 h-3.5" :class="{ 'animate-pulse': props.autoUseImages }" />
</button>
</SlimTooltip>
</div>

<!-- Thinking Mode Toggle -->
<SlimTooltip text="Thinking Mode (CoT)" :placement="compact ? 'top' : 'bottom'">
<button @click="isThinkingMode = !isThinkingMode"
class="flex items-center justify-center p-2 rounded-xl border transition-all active:scale-95 mb-1" :class="[
isThinkingMode
? 'bg-purple-500/10 border-purple-500/30 text-purple-600 dark:text-purple-400 shadow-sm shadow-purple-500/10'
: 'bg-zinc-100 dark:bg-white/5 border-black/5 dark:border-white/10 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300'
]">
<Icon name="iconify tabler--brain" class="w-4 h-4" :class="isThinkingMode ? 'animate-pulse' : ''" />
</button>
</SlimTooltip>

<SlimTooltip :text="submitIcon === 'IconSend' ? 'Send Message (Enter)' : 'Execute Task'"
:placement="compact ? 'top' : 'bottom'">
<IconButton @click="handleSend" :icon="submitIcon" color="primary" :size="compact ? 'xs' : 'sm'" rounded="lg"
:disabled="!internalText.trim() && attachedImages.length === 0" />
</SlimTooltip>
<div class="flex items-center gap-1.5">
<!-- Results Count -->
<SlimTooltip text="Number of result variations to generate" :placement="compact ? 'top' : 'bottom'">
<div
class="flex items-center bg-zinc-100 dark:bg-white/5 rounded-xl px-2 py-1 border border-black/5 dark:border-white/10 group/count hover:border-primary/30 transition-colors"
:class="compact ? 'scale-90 origin-right' : ''">
<span
class="text-[9px] font-black uppercase tracking-widest text-zinc-400 group-hover/count:text-primary/70 transition-colors mr-1.5 select-none pl-1">Samples</span>
<select v-model="resultsCount"
class="bg-transparent border-none text-[11px] font-black text-zinc-700 dark:text-zinc-300 focus:ring-0 p-0 pr-4 cursor-pointer hover:text-primary transition-colors">
<option v-for="n in 5" :key="n" :value="n">{{ n }}</option>
</select>
</div>
</SlimTooltip>

<!-- Thinking Mode Toggle -->
<SlimTooltip text="Thinking Mode (CoT)" :placement="compact ? 'top' : 'bottom'">
<button @click="isThinkingMode = !isThinkingMode"
class="flex items-center justify-center p-2 rounded-xl border transition-all active:scale-95" :class="[
isThinkingMode
? 'bg-purple-500/10 border-purple-500/30 text-purple-600 dark:text-purple-400 shadow-sm shadow-purple-500/10'
: 'bg-zinc-100 dark:bg-white/5 border-black/5 dark:border-white/10 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300'
]">
<Icon name="iconify tabler--brain" class="w-4 h-4" :class="isThinkingMode ? 'animate-pulse' : ''" />
</button>
</SlimTooltip>

<SlimTooltip :text="submitIcon === 'IconSend' ? 'Send Message (Enter)' : 'Execute Task'"
:placement="compact ? 'top' : 'bottom'">
<IconButton @click="handleSend" :icon="submitIcon" color="primary" :size="compact ? 'xs' : 'sm'" rounded="lg"
:disabled="!internalText.trim() && attachedImages.length === 0" />
</SlimTooltip>
</div>
</div>
</div>

<AttachmentModal v-model="showAttachmentModal" @select="handleImagesSelected" />
Expand All @@ -86,14 +112,16 @@ const props = withDefaults(defineProps<{
attachedImages?: string[]
compact?: boolean
submitIcon?: string
autoUseImages?: boolean
}>(), {
placeholder: 'Type a message...',
attachedImages: () => [],
compact: false,
submitIcon: 'IconSend'
submitIcon: 'IconSend',
autoUseImages: false
})

const emit = defineEmits(['update:modelValue', 'update:attachedImages', 'send'])
const emit = defineEmits(['update:modelValue', 'update:attachedImages', 'send', 'update:autoUseImages'])

const internalText = ref(props.modelValue)
const resultsCount = ref(1)
Expand All @@ -115,6 +143,10 @@ const normalizeUrl = (url: string) => {
return url.startsWith('media://') ? url : `media://${url}`
}

const toggleAutoUseImages = () => {
emit('update:autoUseImages', !props.autoUseImages)
}

const adjustTextarea = () => {
if (textareaRef.value) {
textareaRef.value.style.height = 'auto'
Expand Down Expand Up @@ -142,7 +174,7 @@ const removeAttachment = (index: number) => {

const handleSend = () => {
if (!internalText.value.trim() && props.attachedImages.length === 0) return
emit('send', internalText.value, [...props.attachedImages], resultsCount.value, isThinkingMode.value)
emit('send', internalText.value, [...props.attachedImages], resultsCount.value, isThinkingMode.value, props.autoUseImages)

// Clear local state if parent doesn't reset via props (standard pattern)
internalText.value = ''
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/components/chat/ChatInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ const branchedVersion = computed(() => {
return `v.${version}${type}`
})

const handleSend = (text: string, images: string[], count: number, isThinkingMode: boolean) => {
emit('send', text, images, count, isThinkingMode)
const handleSend = (text: string, images: string[], count: number, isThinkingMode: boolean, autoUseImages: boolean) => {
emit('send', text, images, count, isThinkingMode, autoUseImages)
userPrompt.value = ''
attachedImages.value = []
}
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/components/chat/ChatMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ const handleRetry = async () => {
isRetryModalOpen.value = true
}

const handleRetryConfirmed = async (messageId: string, shouldRemoveBranch: boolean, count: number, isThinkingMode: boolean) => {
await videoStore.retryMessage(messageId, shouldRemoveBranch, count, isThinkingMode)
const handleRetryConfirmed = async (messageId: string, shouldRemoveBranch: boolean, count: number, isThinkingMode: boolean, autoUseImages: boolean) => {
await videoStore.retryMessage(messageId, shouldRemoveBranch, count, isThinkingMode, autoUseImages)
emit('retry', messageId)
}

Expand Down
Loading
Loading